diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca52a66a5..5e9d219fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,164 +7,28 @@ on: - main jobs: - change-impact-gate: + check-and-test: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - name: Check out full repository history + - name: Check out repository uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Show public impact plan - continue-on-error: true - run: python docker/debug/gate.py plan --base origin/main - - - name: Enforce append-only Yoyo migrations - run: python scripts/check_yoyo_migrations.py --base origin/main - - - name: Run public semantic gate - run: python docker/debug/gate.py run --base origin/main - - - name: Upload change gate evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: change-impact-gate - path: docker/debug/reports/change-gate/ - - plugin-v3-mobile-gate: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" + cache: pip - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: "22" + cache: npm - - name: Verify locked pure-v3 Mobile fleet - run: python docker/debug/plugin_v3_mobile_gate.py --require-clean-core - - - name: Upload pure-v3 Mobile evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: plugin-v3-mobile-gate - path: docker/debug/reports/plugin-v3-mobile/ - - plugin-v3-fleet-static-gate: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Check out full repository history - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Verify locked pure-v3 fleet and retired-plugin exclusions - run: >- - python docker/debug/plugin_v3_fleet_gate.py - --require-clean-core - --require-full-core-history - - - name: Upload pure-v3 fleet evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: plugin-v3-fleet-static-gate - path: docker/debug/reports/plugin-v3-fleet/ - - plugin-passive-composition-v3-gate: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - cache-dependency-path: requirements.txt - - - name: Install runtime dependencies - run: python -m pip install -r requirements.txt - - - name: Verify locked Citation and Meme v3 composition - run: python docker/debug/plugin_passive_composition_v3_gate.py --require-clean-core - - - name: Verify Citation and Meme through public WebUI - run: python docker/debug/plugin_passive_webui_v3_e2e.py --require-clean-core - - - name: Upload passive v3 composition evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: plugin-passive-composition-v3-gate - path: | - docker/debug/reports/plugin-passive-composition-v3/ - docker/debug/reports/plugin-passive-webui-v3/ - - plugin-composition-v3-gate: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - cache-dependency-path: requirements.txt - - - name: Install runtime dependencies - run: python -m pip install -r requirements.txt - - - name: Verify locked v3 plugin composition - run: python docker/debug/plugin_composition_v3_gate.py --require-clean-core - - - name: Upload v3 plugin composition evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: plugin-composition-v3-gate - path: docker/debug/reports/plugin-composition-v3/ - - check-and-test: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: "pip" - - - name: Install dependencies + - name: Install Python dependencies run: | python -m venv .venv .venv/bin/python -m pip install --upgrade pip @@ -172,15 +36,11 @@ jobs: .venv/bin/python -m pip install -r requirements-dev.txt .venv/bin/python -m pip install -e sdk/python - - name: Run pyright + - name: Check Python run: | .venv/bin/pyright --level error - - name: Run pyright for tests - run: | .venv/bin/pyright --project pyrightconfig.tests.json --level error - - - name: Check control protocol schema - run: .venv/bin/python scripts/generate_control_schema.py --check + .venv/bin/python scripts/generate_control_schema.py --check - name: Check Python SDK run: | @@ -188,78 +48,41 @@ jobs: ../../.venv/bin/pyright src tests --level error ../../.venv/bin/pytest -q tests - - name: Run pytest + - name: Run Python regressions timeout-minutes: 15 env: PYTHONWARNINGS: "ignore:.*AbstractEventLoopPolicy.*:DeprecationWarning" run: | + .venv/bin/python scripts/check_test_budget.py .venv/bin/pytest -q -W error tests/ - docker-control-gate: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Check out repository - uses: actions/checkout@v4 - - - name: Run deterministic control gates + - name: Run Web regressions run: | - python docker/debug/programmatic_control_probe.py --gate smoke - python docker/debug/programmatic_control_probe.py --gate failure-matrix - python docker/debug/programmatic_control_probe.py --gate memory-context - - - name: Upload control gate evidence - if: always() - uses: actions/upload-artifact@v4 - with: - name: programmatic-control-gate - path: docker/debug/reports/programmatic-control/ + npm ci + npm run test:web + npm run typecheck - restart-gate: + change-impact-gate: runs-on: ubuntu-latest - timeout-minutes: 30 + timeout-minutes: 20 steps: - - name: Check out repository + - name: Check out full repository history uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 with: - python-version: "3.13" - cache: pip - cache-dependency-path: requirements.txt - - - name: Install runtime dependencies - run: python -m pip install -r requirements.txt - - - name: Run restart soak gate - run: python docker/debug/restart_probe.py --soak + fetch-depth: 0 - - name: Verify restart cleanup evidence - run: | - python - <<'PY' - import json - import os - from pathlib import Path + - name: Show selected semantic scenarios + run: python docker/debug/gate.py plan --base origin/main - def latest(pattern: str) -> dict[str, object]: - reports = list(Path("docker/debug/reports").glob(pattern)) - if not reports: - raise SystemExit(f"缺少 Gate 报告: {pattern}") - path = max(reports, key=lambda item: item.stat().st_mtime_ns) - return json.loads(path.read_text(encoding="utf-8")) + - name: Enforce append-only Yoyo migrations + run: python scripts/check_yoyo_migrations.py --base origin/main - restart = latest("restart/*/gate.json") - assert restart["status"] == "passed" - assert restart["head"] == os.environ["GITHUB_SHA"] - assert restart["dirtyStatus"] == [] - assert not any(restart["residualResources"].values()) - PY + - name: Run selected semantic scenarios + run: python docker/debug/gate.py run --base origin/main - - name: Upload restart evidence + - name: Upload Gate evidence if: always() uses: actions/upload-artifact@v4 with: - name: restart-gate - path: | - docker/debug/reports/restart/ + name: change-impact-gate + path: docker/debug/reports/change-gate/ diff --git a/.github/workflows/plugin-v3-candidate-gates.yml b/.github/workflows/plugin-v3-candidate-gates.yml new file mode 100644 index 000000000..3cbb840dc --- /dev/null +++ b/.github/workflows/plugin-v3-candidate-gates.yml @@ -0,0 +1,44 @@ +name: Plugin v3 Candidate Gates + +on: + workflow_dispatch: + +jobs: + observable-boundaries: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Check out full repository history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Install runtime dependencies + run: python -m pip install -r requirements.txt + + - name: Verify retained plugin v3 boundaries + run: | + python docker/debug/plugin_v3_fleet_gate.py --require-clean-core --require-full-core-history + python docker/debug/plugin_v3_mobile_gate.py --require-clean-core + python docker/debug/plugin_passive_webui_v3_e2e.py --require-clean-core + + - name: Upload commit-bound evidence + if: always() + uses: actions/upload-artifact@v4 + with: + name: plugin-v3-candidate-${{ github.sha }} + path: | + docker/debug/reports/plugin-v3-mobile/ + docker/debug/reports/plugin-v3-fleet/ + docker/debug/reports/plugin-passive-webui-v3/ diff --git a/.github/workflows/programmatic-control-nightly.yml b/.github/workflows/programmatic-control-nightly.yml index fdfee19c5..4f632adfa 100644 --- a/.github/workflows/programmatic-control-nightly.yml +++ b/.github/workflows/programmatic-control-nightly.yml @@ -1,25 +1,39 @@ -name: Programmatic Control Nightly +name: Runtime Lifecycle Weekly on: schedule: - - cron: "23 18 * * *" + - cron: "23 18 * * 0" workflow_dispatch: jobs: - soak: + lifecycle: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 60 steps: - name: Check out repository uses: actions/checkout@v4 - - name: Run deterministic resource soak - run: python docker/debug/programmatic_control_probe.py --gate soak + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + + - name: Install runtime dependencies + run: python -m pip install -r requirements.txt + + - name: Run deterministic lifecycle gates + run: | + python docker/debug/programmatic_control_probe.py --gate failure-matrix + python docker/debug/programmatic_control_probe.py --gate soak + python docker/debug/restart_probe.py --soak - - name: Upload commit-bound soak evidence + - name: Upload commit-bound lifecycle evidence if: always() uses: actions/upload-artifact@v4 with: - name: programmatic-control-soak-${{ github.sha }} - path: docker/debug/reports/programmatic-control/ + name: runtime-lifecycle-${{ github.sha }} + path: | + docker/debug/reports/programmatic-control/ + docker/debug/reports/restart/ retention-days: 30 diff --git a/docker/debug/README.md b/docker/debug/README.md index 94001e6d0..aa99dbdbb 100644 --- a/docker/debug/README.md +++ b/docker/debug/README.md @@ -33,37 +33,14 @@ python docker/debug/gate.py plan --base origin/main 公开 Gate 不安装也不枚举私有插件,不依赖外部私有验证或 provider 身份;公开报告是当前仓库的合并依据。 -## Citation + Meme 纯 v3 组合 Gate +## Citation + Meme 纯 v3 WebUI Gate -`plugin_passive_composition_v3_gate.py` 从锁文件 fresh checkout 纯 v3 Citation、Meme -与公共插件合同,在临时 workspace 中通过真实 `PluginManager.load_all()` 发布 stable -snapshot。Gate 只从该 snapshot lease 执行 prompt、回复预处理和清理事件,并验证 -Service/Fiber 依赖、Skill、Dashboard、workspace asset 零改写和终止回收。 - -```text -┌─ Citation Fiber ── provide citation.protocol ───────────────┐ -│ ├─ prompt protocol │ -│ ├─ citation metadata │ -│ └─ final protocol cleanup │ -│ ▼ -└─────────────────────────────── Meme Fiber (required inject) - ├─ prompt catalog - ├─ reply media decoration - ├─ meme-manage Skill - └─ Dashboard + workspace/memes -``` - -```bash -python docker/debug/plugin_passive_composition_v3_gate.py --require-clean-core -``` - -证据写入 `docker/debug/reports/plugin-passive-composition-v3/gate.json`。运行期间只写 -临时 checkout、临时 workspace 与被 Git 忽略的报告目录,不读取或修改正式 workspace。 - -同一组 exact commits 还必须通过完整 WebUI runtime:Gate 用 installed stable artifact +Gate 用 exact commits 和 installed stable artifact 布局启动 supervised Gateway,只保留 WebUI channel,经公开 WebSocket 完成一轮回复,再从 公开 HTTP 读取消息、媒体、Dashboard 与 capability。它同时核对模型 prompt 中 Citation→Meme 顺序、SessionDB、artifact 前后摘要以及 Compose 零残留。 +`plugin_passive_composition_v3_gate.py` 仅保留为该 runner 的 exact source、装配和摘要 helper, +不再作为独立 Gate 执行。 ```bash python docker/debug/plugin_passive_webui_v3_e2e.py --require-clean-core @@ -103,7 +80,8 @@ python docker/debug/programmatic_control_probe.py --gate failure-matrix python docker/debug/programmatic_control_probe.py --gate soak ``` -当前基建实现 `smoke`、PR 必选的 `failure-matrix` 和 nightly/release `soak`。`smoke` +当前基建实现 `smoke`、`failure-matrix` 和 `soak`。每周 `Runtime Lifecycle Weekly` +顺序运行 `failure-matrix`、`soak` 与 restart soak;它们不再是普通 PR 的重复 Gate。`smoke` 覆盖 UDS/stdio、基本 turn,以及 streaming/tool/usage 的事件与 DB 一致性; `failure-matrix` 覆盖双连接隔离、同 thread active-start busy、精确中断、断线恢复、慢客户端背压、 provider 分类、非法协议、Web channel parity、workspace lock、SIGTERM 和 crash/restart。 @@ -153,13 +131,12 @@ workspace 手工 TOML、watcher/admin 和独立热重载路径已删除;Gate ```bash python docker/debug/plugin_v3_fleet_gate.py -python docker/debug/plugin_composition_v3_gate.py --require-clean-core python docker/debug/restart_probe.py --soak ``` -每个报告必须记录同一源码 HEAD、manifest/artifact digest、候选与 stable generation、真实 -MCP handshake/readiness、进程/stdio cleanup 和无残留资源;不能用旧 workspace MCP probe -替代 v3 插件 Gate。 +fleet 报告固定来源和 v3-only 静态合同;真实 MCP handshake/readiness、进程/stdio cleanup +由保留的行为回归和每周 restart soak 负责。已删除的 composition Gate 只是按固定插件排列 +重复内部 snapshot,不再作为独立证据。 ## Content / Wake / Drift 真实插件互操作 Gate @@ -351,60 +328,36 @@ container ## 插件变更 Gate -pure-v3 发布证据分成静态 fleet、领域组合与四个集中 E2E 批次。所有 Gate +pure-v3 候选证据由 fleet、Mobile 和公共 WebUI 三个边界组成。所有 Gate 使用 exact commit 锁、一次性 workspace/plugin-home/HOME 与受控端点,不读写正式 Akashic workspace、正式凭据或 hua-home 服务。 +这些是插件候选与发布 Gate,不是普通 Core Pull Request 的固定矩阵。普通 Pull Request +运行全部保留回归和按 diff 选场景的统一变更影响 Gate;当改动进入插件候选时, +`Plugin v3 Candidate Gates` 手动 workflow 只运行 fleet completeness、Mobile 和公共 WebUI。 + ```text -精确 fleet lock +精确能力 lock │ - ├── static fleet ── manifest / api_version=3 / retired exclusions + ├── fleet ─────── 全插件来源、v3-only 与 retired exclusion ├── Mobile ────── Python catalog / JS ABI / plugin tests - ├── Tool ─────── typed prepare / authorize / result - ├── Passive/WebUI ── Citation / Meme / public WebSocket - └── E1─E4 ───── grouped behavior / failure / copied-workspace rehearsal + └── WebUI ─────── Citation / Meme / public WebSocket ``` -静态 fleet 与 Mobile Gate: +候选 workflow 保留的独立边界: ```bash python docker/debug/plugin_v3_fleet_gate.py \ --require-clean-core --require-full-core-history python docker/debug/plugin_v3_mobile_gate.py --require-clean-core -``` - -领域组合 Gate: - -```bash -python docker/debug/plugin_composition_v3_gate.py --require-clean-core -python docker/debug/plugin_passive_composition_v3_gate.py --require-clean-core python docker/debug/plugin_passive_webui_v3_e2e.py --require-clean-core ``` -集中 E2E 只在能力接线全部完成后运行一轮: - -```bash -python docker/debug/plugin_v3_e1_gate.py -python docker/debug/plugin_v3_e2_gate.py --require-clean-core -python docker/debug/plugin_v3_e4_gate.py \ - --source-workspace /path/to/source-workspace \ - --source-config /path/to/config.toml \ - --plugin-home /path/to/plugin-home -``` - -所有集中 Gate 默认使用 Python/操作系统选择的临时目录;E1、E2 与 E4 可通过 `--tmp-root` -显式选择已有目录。测试源码不绑定维护者 HOME、正式 workspace 或一次性试运行路径。 - -E1 覆盖 Akasha、Citation/Meme、Observe、Emotion、Proactive Feedback 与 -Plugin Undo;E2 覆盖 Shell 三件与 MCP/process 插件;E4 覆盖正式来源 workspace 的组合激活边界。E4 不重复逐插件运行,而是从同一 Core head -的 E1~E3 报告建立覆盖集,再在复制 workspace 中验证 SQLite 完整性、messages -只追加、plugin-data 权威文件与 artifact/pointer 不变,以及进程内失败/子进程崩溃恢复。 -SQLite 在线备份可能在只读源旁创建或触碰 `-wal`/`-shm`/`-journal` 运行 sidecar; -E4 不把这些可重建 sidecar 计入 plugin-data 身份,但仍逐字节固定主数据库和其他文件。 - -报告中任何 `blocked`、不同 Core head、非 exact lock、未覆盖 fleet 或 cleanup 残留都会令 -最终 rehearsal 非零退出。正式 workspace 备份和 hua-home 切换不属于这些 Gate -的授权范围。 +被删除的 E1/E2 固定了 2026-08 的组合 API;Core 在 2026-09-02 删除 v2 兼容后, +锁定的 Emotion 和 Calendar 插件分别仍导入已删除的 `CoreEvent` 与 +`PROACTIVE_COMPONENTS`,所以两条 Gate 只会阻止当前合法演进。E4 又依赖 E1 和仓库中 +不存在的 E3 runner,无法形成可执行发布合同,也一并删除。正式 workspace 的发布验收 +应在拥有真实部署输入的发布流程中重建,不能由仓库内永远 blocked 的脚本冒充。 ## 第一次启动 diff --git a/docker/debug/content-source-interop.lock.json b/docker/debug/content-source-interop.lock.json index 13f0bcf4a..d517b9abc 100644 --- a/docker/debug/content-source-interop.lock.json +++ b/docker/debug/content-source-interop.lock.json @@ -2,10 +2,9 @@ "schema_version": 1, "core_contract": "39cbdcefc155aaf6c41deafd7754a37e6126c23c", "core_cases": [ - "tests/test_content_v3_composition.py", - "tests/test_wake_v3_composition.py", "tests/test_wake_durable_delivery.py", - "tests/test_wake_drift_fixture.py" + "tests/test_wake_gate_contract.py", + "tests/semantic/test_companion_contract.py::test_content_delivery_rejects_early_source_ack_mutant" ], "coexistence": [ { diff --git a/docker/debug/content-wake-h5.manifest.json b/docker/debug/content-wake-h5.manifest.json index 99a400f02..1d1e58d45 100644 --- a/docker/debug/content-wake-h5.manifest.json +++ b/docker/debug/content-wake-h5.manifest.json @@ -4,24 +4,24 @@ "suites": [ { "id": "scheduler", - "cases": ["tests/test_scheduler_v3_shadow.py"] + "cases": [ + "tests/semantic/test_companion_contract.py::test_schedule_capacity_rejects_unbounded_add_mutant", + "tests/test_job_store.py" + ] }, { "id": "subagent_and_real_mcp", "cases": [ - "tests/test_subagent_v3_shadow.py", - "tests/test_subagent_v3_runtime.py", - "tests/test_plugin_composition_generation_host.py::test_exact_root_candidate_materializes_process_mcp_and_tool_route", - "tests/test_mcp_process_recovery.py::test_mcp_client_recovers_process_epoch_and_keeps_logical_contract" + "tests/test_mcp_process_recovery.py::test_mcp_client_recovers_process_epoch_and_keeps_logical_contract", + "tests/test_plugin_managed_process_host.py::test_process_exit_recovers_with_new_epoch_without_stale_resurrection" ] }, { "id": "wake_drift_and_handoff", "cases": [ - "tests/test_wake_v3_composition.py", "tests/test_wake_durable_delivery.py", - "tests/test_wake_drift_fixture.py", - "tests/test_proactive_island_handoff.py" + "tests/test_wake_gate_contract.py", + "tests/test_proactive_feedback_emotion_interop.py" ] } ], diff --git a/docker/debug/plugin-v3-fleet.lock.json b/docker/debug/plugin-v3-fleet.lock.json index 05266327e..e254efe11 100644 --- a/docker/debug/plugin-v3-fleet.lock.json +++ b/docker/debug/plugin-v3-fleet.lock.json @@ -32,16 +32,16 @@ { "id": "calendar-mcp", "repository": "https://github.com/akashic-plugins/calendar-mcp", - "requested_ref": "293d6ba824950ee087e05bfcb02187339d840973", - "resolved_sha": "293d6ba824950ee087e05bfcb02187339d840973", - "change_source_pr_head": "293d6ba824950ee087e05bfcb02187339d840973" + "requested_ref": "048c8e809559f0e4f25b2633e7f8606e37129ca1", + "resolved_sha": "048c8e809559f0e4f25b2633e7f8606e37129ca1", + "change_source_pr_head": "048c8e809559f0e4f25b2633e7f8606e37129ca1" }, { "id": "emotion", "repository": "https://github.com/akashic-plugins/emotion", - "requested_ref": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d", - "resolved_sha": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d", - "change_source_pr_head": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d" + "requested_ref": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2", + "resolved_sha": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2", + "change_source_pr_head": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2" }, { "id": "plugin_undo", @@ -53,9 +53,9 @@ { "id": "observe", "repository": "https://github.com/akashic-plugins/observe", - "requested_ref": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183", - "resolved_sha": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183", - "change_source_pr_head": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183" + "requested_ref": "09214c23f287f659eee6280706208b9ba7d2ed13", + "resolved_sha": "09214c23f287f659eee6280706208b9ba7d2ed13", + "change_source_pr_head": "09214c23f287f659eee6280706208b9ba7d2ed13" }, { "id": "setup_helper", @@ -74,9 +74,9 @@ { "id": "feed-mcp", "repository": "https://github.com/akashic-plugins/feed-mcp", - "requested_ref": "91f645ab24742acd8200f51db48561c1e1515604", - "resolved_sha": "91f645ab24742acd8200f51db48561c1e1515604", - "change_source_pr_head": "91f645ab24742acd8200f51db48561c1e1515604" + "requested_ref": "dccbcd90d9c03e56ccfd5ef8451e13cb0cbc637f", + "resolved_sha": "dccbcd90d9c03e56ccfd5ef8451e13cb0cbc637f", + "change_source_pr_head": "dccbcd90d9c03e56ccfd5ef8451e13cb0cbc637f" }, { "id": "feishu", @@ -88,16 +88,16 @@ { "id": "fitbit-mcp", "repository": "https://github.com/akashic-plugins/fitbit-mcp", - "requested_ref": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34", - "resolved_sha": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34", - "change_source_pr_head": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34" + "requested_ref": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80", + "resolved_sha": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80", + "change_source_pr_head": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80" }, { "id": "steam-mcp", "repository": "https://github.com/akashic-plugins/steam-mcp", - "requested_ref": "337c8f1898e3d4e23c02da0d78e86877d10940d2", - "resolved_sha": "337c8f1898e3d4e23c02da0d78e86877d10940d2", - "change_source_pr_head": "337c8f1898e3d4e23c02da0d78e86877d10940d2" + "requested_ref": "d2ddd1bc8766f0c90e13ff8b5e2fe187c0429a94", + "resolved_sha": "d2ddd1bc8766f0c90e13ff8b5e2fe187c0429a94", + "change_source_pr_head": "d2ddd1bc8766f0c90e13ff8b5e2fe187c0429a94" }, { "id": "qqbot", diff --git a/docker/debug/plugin-v3-mobile.lock.json b/docker/debug/plugin-v3-mobile.lock.json index 917c5f560..6a2700619 100644 --- a/docker/debug/plugin-v3-mobile.lock.json +++ b/docker/debug/plugin-v3-mobile.lock.json @@ -17,9 +17,9 @@ "id": "observe", "source": "external", "repository": "https://github.com/akashic-plugins/observe", - "requested_ref": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183", - "resolved_sha": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183", - "change_source_pr_head": "06e4f4b13223cb5621c19f4dbf0fa160c5bdb183", + "requested_ref": "09214c23f287f659eee6280706208b9ba7d2ed13", + "resolved_sha": "09214c23f287f659eee6280706208b9ba7d2ed13", + "change_source_pr_head": "09214c23f287f659eee6280706208b9ba7d2ed13", "entrypoint": "plugin.py", "module": "mobile_panel.js", "stylesheet": "mobile_panel.css", @@ -32,9 +32,9 @@ "id": "fitbit-mcp", "source": "external", "repository": "https://github.com/akashic-plugins/fitbit-mcp", - "requested_ref": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34", - "resolved_sha": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34", - "change_source_pr_head": "eda9a879c751f4d2268a3dbc4c7b1f847de79f34", + "requested_ref": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80", + "resolved_sha": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80", + "change_source_pr_head": "e0eda11d822e2ca0cf4abeee1e3ef93d60cc1a80", "entrypoint": "plugin.py", "module": "mobile_panel.js", "stylesheet": "mobile_panel.css", @@ -62,9 +62,9 @@ "id": "emotion", "source": "external", "repository": "https://github.com/akashic-plugins/emotion", - "requested_ref": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d", - "resolved_sha": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d", - "change_source_pr_head": "3d14315ec91dc40eb2e2cf9ba66411f12dd20d3d", + "requested_ref": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2", + "resolved_sha": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2", + "change_source_pr_head": "d828fd7ec97e027bc1ee4a39e5501a2cf25296a2", "entrypoint": "plugin.py", "module": "mobile_panel.js", "stylesheet": "mobile_panel.css", diff --git a/docker/debug/plugin_composition_v3_gate.py b/docker/debug/plugin_composition_v3_gate.py deleted file mode 100644 index d5a307665..000000000 --- a/docker/debug/plugin_composition_v3_gate.py +++ /dev/null @@ -1,456 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import hashlib -import json -import re -import subprocess -import sys -import tempfile -from dataclasses import asdict, dataclass -from datetime import UTC, datetime -from pathlib import Path -from collections.abc import Awaitable, Callable -from typing import Any, cast - -ROOT = Path(__file__).resolve().parents[2] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from agent.plugins.manager import PluginManager # noqa: E402 -from agent.plugins.snapshot import ( # noqa: E402 - bind_runtime_snapshot, - reset_runtime_snapshot, -) -from agent.tools.events import ( # noqa: E402 - ToolExecutionRequest, - ToolExecutionResult, -) -from agent.tools.executor import ToolExecutor # noqa: E402 -from bus.event_bus import EventBus # noqa: E402 - - -DEFAULT_LOCK = ROOT / "docker" / "debug" / "plugin-composition-v3.lock.json" -DEFAULT_REPORT = ( - ROOT / "docker" / "debug" / "reports" / "plugin-composition-v3" / "gate.json" -) -COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}") -GATE_VERSION = 1 -PROTOCOL_SOURCE_REPOSITORY = "https://github.com/kachofugetsu09/akashic-agent.git" -PROTOCOL_SOURCE_COMMIT = "0940e9e74a62efef54470f11a7064a99ca5e9acc" -PROTOCOL_SOURCE_PATHS = ( - "agent/tools/events.py", - "agent/tools/executor.py", -) -EXPECTED_PLUGIN_IDS = ("shell_restore", "shell_safety") -SCENARIO_PROFILE = "plugin-tool-v3-v1" -EXPECTED_LISTENERS = ( - "transform:tool.input.prepare[akashic.tool-input.v1]:shell_restore", - "serial:tool.execution.authorize[bail=akashic.tool-deny-reason.v1]:shell_safety", -) -ToolInvoker = Callable[[str, dict[str, Any]], Awaitable[Any]] - - -@dataclass(frozen=True) -class PluginLock: - id: str - repository: str - requested_ref: str - resolved_sha: str - change_source_pr_head: str - - -@dataclass(frozen=True) -class PluginEvidence: - id: str - repository: str - requested_ref: str - resolved_sha: str - change_source_pr_head: str - tree: str - - -@dataclass(frozen=True) -class ScenarioEvidence: - id: str - status: str - final_command: str - invoked: bool - - -@dataclass(frozen=True) -class ScenarioCase: - id: str - session: str - command: str - expected_status: str - expected_invoked: bool - - -@dataclass(frozen=True) -class CleanupEvidence: - listeners: tuple[str, ...] - effects: tuple[str, ...] - - -SCENARIO_CATALOG = ( - ScenarioCase("plain-rm", "plain", "rm /tmp/plain.txt", "success", True), - ScenarioCase( - "sudo-cluster", - "cluster", - "sudo -nE rm /tmp/cluster.txt", - "success", - True, - ), - ScenarioCase( - "sudo-preserve-env", - "env", - "sudo -n --preserve-env=HOME rm /tmp/env.txt", - "success", - True, - ), - ScenarioCase( - "sudo-mode-denied", - "mode", - "sudo -n -s rm /tmp/mode.txt", - "denied", - False, - ), - ScenarioCase("repeat-1", "repeat", "rm /tmp/repeat.txt", "success", True), - ScenarioCase("repeat-2", "repeat", "rm /tmp/repeat.txt", "success", True), - ScenarioCase("repeat-3", "repeat", "rm /tmp/repeat.txt", "success", True), -) - - -def main() -> None: - """Checkout exact plugins and verify their composed stable runtime behavior.""" - - args = _parse_args() - core_status = _git_output(ROOT, "status", "--porcelain").splitlines() - if args.require_clean_core and core_status: - raise RuntimeError(f"核心工作树不干净: {core_status}") - locks = _load_lock(args.lock.resolve()) - - with tempfile.TemporaryDirectory(prefix="akashic-plugin-composition-v3-") as raw: - sandbox = Path(raw) - providers = sandbox / "providers" - providers.mkdir() - plugin_evidence = tuple( - _checkout_locked_plugin(lock, providers / lock.id) for lock in locks - ) - listeners, scenarios, invocations, cleanup = asyncio.run( - _verify_composition(providers, sandbox) - ) - - report = { - "status": "passed", - "gate_version": GATE_VERSION, - "checked_at": datetime.now(UTC).isoformat(), - "core": { - "head": _git_output(ROOT, "rev-parse", "HEAD"), - "tree": _git_output(ROOT, "rev-parse", "HEAD^{tree}"), - "dirty_status": core_status, - }, - "lock": str(args.lock.resolve().relative_to(ROOT)), - "lock_sha256": _sha256(args.lock.resolve()), - "protocol_source": _protocol_source_evidence(), - "plugins": [asdict(item) for item in plugin_evidence], - "topology_listeners": list(listeners), - "scenario_profile": SCENARIO_PROFILE, - "scenario_catalog_sha256": _scenario_catalog_sha256(), - "scenario_catalog": [asdict(item) for item in SCENARIO_CATALOG], - "scenarios": [asdict(item) for item in scenarios], - "invocations": invocations, - "cleanup": asdict(cleanup), - } - report_path = args.report.resolve() - report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(f"plugin composition v3 gate passed: {report_path}") - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="验证固定 v3 插件的组合执行合同") - parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) - parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) - parser.add_argument("--require-clean-core", action="store_true") - return parser.parse_args() - - -def _load_lock(path: Path) -> tuple[PluginLock, ...]: - """Strictly load the immutable cross-repository plugin set.""" - - raw = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(raw, dict) or set(raw) != {"schema_version", "plugins"}: - raise ValueError("v3 插件组合锁根结构无效") - if raw["schema_version"] != 1: - raise ValueError(f"不支持的 v3 插件组合锁版本: {raw['schema_version']}") - raw_plugins = raw["plugins"] - if not isinstance(raw_plugins, list): - raise ValueError("v3 插件组合锁 plugins 必须是列表") - plugins = tuple(_parse_plugin_lock(item) for item in raw_plugins) - if tuple(item.id for item in plugins) != EXPECTED_PLUGIN_IDS: - raise ValueError("v3 插件组合锁的插件集合或顺序错误") - return plugins - - -def _parse_plugin_lock(raw: object) -> PluginLock: - expected = { - "id", - "repository", - "requested_ref", - "resolved_sha", - "change_source_pr_head", - } - if not isinstance(raw, dict) or set(raw) != expected: - raise ValueError(f"v3 插件组合锁字段无效: {raw}") - item = cast(dict[str, object], raw) - values = {name: _required_string(item, name) for name in expected} - repository = values["repository"] - if not repository.startswith("https://github.com/") or not repository.endswith(".git"): - raise ValueError(f"插件仓库必须是公开 GitHub HTTPS Git 地址: {repository}") - for field in ("requested_ref", "resolved_sha", "change_source_pr_head"): - if COMMIT_PATTERN.fullmatch(values[field]) is None: - raise ValueError(f"{field} 必须是完整 SHA: {values[field]}") - if len({values[field] for field in ("requested_ref", "resolved_sha", "change_source_pr_head")}) != 1: - raise ValueError(f"试点插件必须把三个 revision 固定到同一提交: {values['id']}") - return PluginLock( - id=values["id"], - repository=repository, - requested_ref=values["requested_ref"], - resolved_sha=values["resolved_sha"], - change_source_pr_head=values["change_source_pr_head"], - ) - - -def _required_string(item: dict[str, object], name: str) -> str: - value = item[name] - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"v3 插件组合锁字段必须是非空字符串: {name}") - return value - - -def _checkout_locked_plugin(lock: PluginLock, checkout: Path) -> PluginEvidence: - """Fetch only one declared public Git object into a fresh repository.""" - - _run(("git", "init", "--quiet", str(checkout)), cwd=ROOT) - _run(("git", "remote", "add", "origin", lock.repository), cwd=checkout) - _run( - ("git", "fetch", "--quiet", "--depth=1", "origin", lock.resolved_sha), - cwd=checkout, - ) - _run(("git", "checkout", "--quiet", "--detach", "FETCH_HEAD"), cwd=checkout) - if _git_output(checkout, "rev-parse", "HEAD") != lock.resolved_sha: - raise RuntimeError(f"插件检出提交与锁不一致: {lock.id}") - if _git_output(checkout, "status", "--porcelain"): - raise RuntimeError(f"插件检出后工作树不干净: {lock.id}") - return PluginEvidence( - id=lock.id, - repository=lock.repository, - requested_ref=lock.requested_ref, - resolved_sha=lock.resolved_sha, - change_source_pr_head=lock.change_source_pr_head, - tree=_git_output(checkout, "rev-parse", "HEAD^{tree}"), - ) - - -async def _verify_composition( - providers: Path, - sandbox: Path, -) -> tuple[ - tuple[str, ...], - tuple[ScenarioEvidence, ...], - list[dict[str, object]], - CleanupEvidence, -]: - """Load one stable Root and execute the migration interaction matrix.""" - - workspace = sandbox / "workspace" - manager = PluginManager( - plugin_dirs=[providers], - event_bus=EventBus(), - tool_registry=None, - workspace=workspace, - installed_cache_root=sandbox / "plugin-home" / "cache", - ) - root = None - result: tuple[ - tuple[str, ...], - tuple[ScenarioEvidence, ...], - list[dict[str, object]], - ] | None = None - try: - await manager.load_all() - snapshot = manager.current_snapshot - if snapshot is None or snapshot.composition_root is None: - raise RuntimeError("正式 snapshot 缺少 v3 CompositionRoot") - root = snapshot.composition_root - topology = root.topology_view() - if topology.listeners != EXPECTED_LISTENERS: - raise RuntimeError(f"v3 listener 顺序不符合组合合同: {topology.listeners}") - - restore = manager.generation("shell_restore") - if restore is None: - raise RuntimeError("正式 snapshot 缺少 shell_restore generation") - restore_dir = restore.data_dir / "restore" - executor = ToolExecutor() - invocations: list[dict[str, object]] = [] - - async def invoke(tool_name: str, arguments: dict[str, Any]) -> str: - invocations.append({"tool_name": tool_name, "arguments": dict(arguments)}) - return "invoked" - - lease = manager.snapshot_store.lease() - token = bind_runtime_snapshot(lease) - try: - scenarios = await _run_scenarios( - executor, - invoke, - restore_dir, - invocations, - ) - finally: - reset_runtime_snapshot(token) - await lease.release() - - expected_invocations = sum(case.expected_invoked for case in SCENARIO_CATALOG) - if len(invocations) != expected_invocations: - raise RuntimeError( - "真实 invoker 调用次数错误: " - f"expected={expected_invocations} actual={len(invocations)}" - ) - result = topology.listeners, scenarios, invocations - finally: - await manager.terminate_all() - if root is None or result is None: - raise AssertionError("组合 Gate 成功路径没有保留正式 Root 结果") - cleanup = CleanupEvidence( - listeners=root.topology_view().listeners, - effects=root.receipt().effects, - ) - if cleanup.listeners or cleanup.effects: - raise RuntimeError(f"正式 Root 终止后仍有组合资源: {cleanup}") - return *result, cleanup - - -async def _run_scenarios( - executor: ToolExecutor, - invoker: ToolInvoker, - restore_dir: Path, - invocations: list[dict[str, object]], -) -> tuple[ScenarioEvidence, ...]: - evidence: list[ScenarioEvidence] = [] - for index, case in enumerate(SCENARIO_CATALOG): - before = len(invocations) - result = await executor.execute( - ToolExecutionRequest( - call_id=f"gate-{index}", - tool_name="shell", - arguments={"command": case.command}, - source="passive", - session_key=case.session, - ), - invoker, - ) - invoked = len(invocations) == before + 1 - if invoked is not case.expected_invoked: - raise RuntimeError( - f"场景 {case.id} invoker 状态错误: " - f"expected={case.expected_invoked} actual={invoked}" - ) - final_command = str(result.final_arguments.get("command", "")) - _assert_scenario( - case.id, - result, - final_command, - restore_dir, - case.expected_status, - ) - evidence.append( - ScenarioEvidence( - id=case.id, - status=result.status, - final_command=final_command, - invoked=invoked, - ) - ) - return tuple(evidence) - - -def _assert_scenario( - case_id: str, - result: ToolExecutionResult, - final_command: str, - restore_dir: Path, - expected_status: str, -) -> None: - if result.status != expected_status: - raise RuntimeError(f"场景 {case_id} 状态错误: {result.status} {result.output}") - if expected_status == "success": - if " mv " not in f" {final_command} " or str(restore_dir) not in final_command: - raise RuntimeError(f"场景 {case_id} 未把 rm 改写到插件数据根: {final_command}") - elif case_id == "sudo-mode-denied": - if "普通命令执行" not in str(result.output): - raise RuntimeError(f"场景 {case_id} 未由 Safety 拒绝 sudo mode: {result.output}") - - -def _sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def _scenario_catalog_sha256() -> str: - encoded = json.dumps( - [asdict(item) for item in SCENARIO_CATALOG], - ensure_ascii=False, - separators=(",", ":"), - sort_keys=True, - ).encode() - return hashlib.sha256(encoded).hexdigest() - - -def _protocol_source_evidence() -> dict[str, object]: - files: list[dict[str, str]] = [] - for path in PROTOCOL_SOURCE_PATHS: - blob = _git_output(ROOT, "rev-parse", f"{PROTOCOL_SOURCE_COMMIT}:{path}") - content = subprocess.run( - ("git", "cat-file", "blob", blob), - cwd=ROOT, - check=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ).stdout - files.append( - { - "path": path, - "git_blob": blob, - "sha256": hashlib.sha256(content).hexdigest(), - } - ) - return { - "repository": PROTOCOL_SOURCE_REPOSITORY, - "commit": PROTOCOL_SOURCE_COMMIT, - "files": files, - } - - -def _git_output(cwd: Path, *args: str) -> str: - return _run(("git", *args), cwd=cwd).stdout.strip() - - -def _run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]: - return subprocess.run( - command, - cwd=cwd, - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - -if __name__ == "__main__": - main() diff --git a/docker/debug/plugin_passive_composition_v3_gate.py b/docker/debug/plugin_passive_composition_v3_gate.py index 64fd30b89..88183bce6 100644 --- a/docker/debug/plugin_passive_composition_v3_gate.py +++ b/docker/debug/plugin_passive_composition_v3_gate.py @@ -1,6 +1,5 @@ from __future__ import annotations -import argparse import asyncio import hashlib import inspect @@ -38,14 +37,6 @@ from bus.event_bus import EventBus # noqa: E402 DEFAULT_LOCK = ROOT / "docker" / "debug" / "plugin-passive-composition-v3.lock.json" -DEFAULT_REPORT = ( - ROOT - / "docker" - / "debug" - / "reports" - / "plugin-passive-composition-v3" - / "gate.json" -) COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}") GATE_VERSION = 1 PROTOCOL_SOURCE_REPOSITORY = "https://github.com/kachofugetsu09/akashic-agent.git" @@ -129,88 +120,6 @@ class CleanupEvidence: dashboard_module_loaded: bool -def main() -> None: - """Checkout exact sources and verify the stable passive composition boundary.""" - - # 1. Freeze the Core and cross-repository evidence identities. - args = _parse_args() - core_status = _git_output(ROOT, "status", "--porcelain").splitlines() - if args.require_clean_core and core_status: - raise RuntimeError(f"核心工作树不干净: {core_status}") - lock = _load_lock(args.lock.resolve()) - - # 2. Build one isolated formal runtime from fresh exact-commit checkouts. - with tempfile.TemporaryDirectory(prefix="akashic-passive-v3-") as raw: - sandbox = Path(raw) - contract_checkout = sandbox / "contract" - contract_evidence = _checkout_locked_source(lock.contract, contract_checkout) - providers = sandbox / "providers" - providers.mkdir() - plugin_evidence = tuple( - _checkout_locked_source(item, providers / item.id) for item in lock.plugins - ) - contract_report = _verify_static_contract( - contract_checkout, - tuple(providers / item.id / "plugin.py" for item in lock.plugins), - ) - runtime = asyncio.run(_verify_composition(providers, sandbox)) - - # 3. Persist reconstructible evidence outside the disposable sandbox. - report = _build_report( - core_status=core_status, - lock_path=args.lock.resolve(), - contract_evidence=contract_evidence, - contract_report=contract_report, - plugin_evidence=plugin_evidence, - runtime=runtime, - ) - report_path = args.report.resolve() - report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(f"passive plugin composition v3 gate passed: {report_path}") - - -def _build_report( - *, - core_status: list[str], - lock_path: Path, - contract_evidence: SourceEvidence, - contract_report: ContractEvidence, - plugin_evidence: tuple[SourceEvidence, ...], - runtime: dict[str, object], -) -> dict[str, object]: - return { - "status": "passed", - "gate_version": GATE_VERSION, - "checked_at": datetime.now(UTC).isoformat(), - "core": { - "head": _git_output(ROOT, "rev-parse", "HEAD"), - "tree": _git_output(ROOT, "rev-parse", "HEAD^{tree}"), - "dirty_status": core_status, - }, - "lock": str(lock_path.relative_to(ROOT)), - "lock_sha256": _sha256(lock_path), - "protocol_source": _protocol_source_evidence(), - "contract_source": asdict(contract_evidence), - "contract_report": asdict(contract_report), - "plugins": [asdict(item) for item in plugin_evidence], - "scenario_profile": SCENARIO_PROFILE, - "scenario_catalog_sha256": _scenario_catalog_sha256(), - **runtime, - } - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="验证 Citation + Meme 纯 v3 组合") - parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) - parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) - parser.add_argument("--require-clean-core", action="store_true") - return parser.parse_args() - - def _load_lock(path: Path) -> GateLock: """Strictly load the immutable protocol and plugin source set.""" @@ -608,7 +517,3 @@ def _run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[ stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - - -if __name__ == "__main__": - main() diff --git a/docker/debug/plugin_v3_e1_gate.py b/docker/debug/plugin_v3_e1_gate.py deleted file mode 100644 index 24ff6dcdd..000000000 --- a/docker/debug/plugin_v3_e1_gate.py +++ /dev/null @@ -1,898 +0,0 @@ -"""集中式、一次性 workspace 的 pure-v3 E1 Gate。""" - -from __future__ import annotations - -import argparse -import asyncio -import hashlib -import json -import sqlite3 -import sys -import tempfile -from dataclasses import asdict, dataclass -from datetime import UTC, datetime -from pathlib import Path -from typing import Any, cast - -ROOT = Path(__file__).resolve().parents[2] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from agent.plugins.generation_activity_host import ActivityHost # noqa: E402 -from agent.plugins.generation_job_host import BackgroundJobActivityAdapter # noqa: E402 -from agent.plugins.manager import PluginManager # noqa: E402 -from agent.plugins.mobile_ui import PluginMobileUiProvider # noqa: E402 -from agent.plugin_composition import ( # noqa: E402 - CompositionRoot, - EMBEDDING_MEMORY_PLUGIN, -) -from agent.tools.registry import ToolRegistry # noqa: E402 -from bus.event_bus import EventBus # noqa: E402 -from session.manager import SessionManager # noqa: E402 - -try: - from docker.debug import plugin_v3_fleet_gate as fleet_gate # noqa: E402 -except ModuleNotFoundError: # pragma: no cover - import plugin_v3_fleet_gate as fleet_gate # type: ignore[no-redef] # noqa: E402 - - -DEFAULT_LOCK = ROOT / "docker" / "debug" / "plugin-v3-fleet.lock.json" -DEFAULT_REPORT = ROOT / "docker" / "debug" / "reports" / "plugin-v3-e1" / "gate.json" -DEFAULT_PASSIVE_WEBUI_REPORT = ( - ROOT / "docker" / "debug" / "reports" / "plugin-passive-webui-v3" / "gate.json" -) -E1_PLUGIN_IDS = ( - "akasha", - "citation", - "meme", - "emotion", - "observe", - "proactive_feedback", - "plugin_undo", -) -E1_EXTERNAL_PLUGIN_IDS = E1_PLUGIN_IDS[1:] -PASSIVE_WEBUI_SCENARIO_PROFILE = "citation-meme-webui-v3-v1" -PASSIVE_WEBUI_PLUGIN_IDS: tuple[str, ...] = ("citation", "meme") -BUILTIN_PLUGIN_ROOTS = { - "models": ROOT / "plugins" / "models", - "akasha": ROOT / "plugins" / "akasha", -} - - -class E1GateError(RuntimeError): - """报告可复现的 E1 输入或证据失败。""" - - -@dataclass(frozen=True, slots=True) -class RuntimeBundle: - """保存一个 disposable Core runtime 的 owner。""" - - workspace: Path - engine_name: str - sessions: SessionManager - manager: PluginManager - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="运行 pure-v3 集中式 E1 Gate") - _ = parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) - _ = parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) - _ = parser.add_argument( - "--passive-webui-report", type=Path, default=DEFAULT_PASSIVE_WEBUI_REPORT - ) - _ = parser.add_argument("--tmp-root", type=Path) - _ = parser.add_argument( - "--plugin-root", action="append", default=[], metavar="PLUGIN_ID=PATH" - ) - _ = parser.add_argument("--offline", action="store_true") - return parser.parse_args() - - -def _parse_plugin_roots(raw: list[str]) -> dict[str, Path]: - """在 CLI 边界解析 explicit exact checkout 路径。""" - - roots: dict[str, Path] = {} - for value in raw: - plugin_id, separator, path = value.partition("=") - if not separator or not plugin_id.strip() or not path.strip(): - raise E1GateError(f"--plugin-root 必须是 PLUGIN_ID=PATH: {value!r}") - if plugin_id in roots: - raise E1GateError(f"--plugin-root 重复: {plugin_id}") - roots[plugin_id] = Path(path).expanduser().resolve(strict=False) - unknown = sorted(set(roots) - set(E1_EXTERNAL_PLUGIN_IDS)) - if unknown: - raise E1GateError(f"--plugin-root 只允许 E1 external plugin: {unknown}") - return roots - - -def _select_e1_locks(path: Path) -> dict[str, Any]: - """从完整 immutable fleet lock 选择 E1 external revisions。""" - - locks = fleet_gate._load_lock(path) # pyright: ignore[reportPrivateUsage] - by_id = {item.id: item for item in locks} - missing = sorted(set(E1_EXTERNAL_PLUGIN_IDS).difference(by_id)) - if missing: - raise E1GateError(f"E1 lock 缺少 external plugin: {missing}") - return {plugin_id: by_id[plugin_id] for plugin_id in E1_EXTERNAL_PLUGIN_IDS} - - -def _local_checkout_evidence(lock: Any, root: Path) -> dict[str, object]: - """验证 local checkout 的 commit、tree、clean 状态。""" - - if not root.is_dir() or root.is_symlink(): - raise E1GateError(f"checkout 不是实体目录: {root}") - actual = fleet_gate._git_output( - root, "rev-parse", "HEAD" - ) # pyright: ignore[reportPrivateUsage] - if actual != lock.resolved_sha: - raise E1GateError( - f"checkout SHA 与锁不一致: {lock.id} expected={lock.resolved_sha} actual={actual}" - ) - dirty = tuple( - fleet_gate._git_output(root, "status", "--porcelain").splitlines() - ) # pyright: ignore[reportPrivateUsage] - if dirty: - raise E1GateError(f"checkout 工作树不干净: {lock.id}: {dirty}") - return { - "id": lock.id, - "repository": lock.repository, - "resolved_sha": lock.resolved_sha, - "tree": fleet_gate._git_output( - root, "rev-parse", "HEAD^{tree}" - ), # pyright: ignore[reportPrivateUsage] - "clean": True, - "history": fleet_gate._git_output( - root, "rev-parse", "--is-shallow-repository" - ), # pyright: ignore[reportPrivateUsage] - "path": str(root), - "mode": "provided-checkout", - } - - -def _resolve_external_roots( - locks: dict[str, Any], staging: Path, provided: dict[str, Path], *, offline: bool -) -> tuple[dict[str, Path], list[dict[str, object]], list[str]]: - """解析 exact lock checkout;绝不回退到旧 v2 checkout。""" - - roots: dict[str, Path] = {} - evidence: list[dict[str, object]] = [] - blockers: list[str] = [] - for plugin_id in E1_EXTERNAL_PLUGIN_IDS: - lock = locks[plugin_id] - try: - if plugin_id in provided: - checkout = provided[plugin_id] - item = _local_checkout_evidence(lock, checkout) - elif offline: - raise E1GateError( - f"offline 模式没有精确 checkout: {plugin_id}@{lock.resolved_sha}" - ) - else: - checkout = staging / plugin_id - result = fleet_gate._checkout_locked_plugin( - lock, checkout - ) # pyright: ignore[reportPrivateUsage] - item: dict[str, object] = { - **cast(dict[str, object], asdict(result)), - "path": str(checkout), - "mode": "shallow-lock-checkout", - } - root = Path(str(item["path"])) - static = fleet_gate._inspect_static_plugin( - root, plugin_id - ) # pyright: ignore[reportPrivateUsage] - item["static"] = static - if static["status"] != "passed": - raise E1GateError(f"external static v3 inspection failed: {plugin_id}") - roots[plugin_id] = root - evidence.append(cast(dict[str, object], item)) - except Exception as error: - blockers.append( - f"{plugin_id}: exact locked checkout unavailable: {type(error).__name__}: {error}" - ) - evidence.append( - { - "id": plugin_id, - "resolved_sha": lock.resolved_sha, - "status": "blocked", - "error": f"{type(error).__name__}: {error}", - "mode": "not-run", - } - ) - return roots, evidence, blockers - - -def _report_object(value: object, label: str) -> dict[str, object]: - if not isinstance(value, dict): - raise E1GateError(f"{label} 必须是 object") - return cast(dict[str, object], value) - - -def _report_index(value: object, label: str) -> int: - if isinstance(value, bool) or not isinstance(value, int) or value < 0: - raise E1GateError(f"{label} 必须是非负整数") - return value - - -def _validate_passive_sources( - report: dict[str, object], locks: dict[str, Any] -) -> dict[str, str]: - """验证 WebUI 报告只引用 E1 fleet 锁定的 Citation/Meme source。""" - - missing_locks = sorted(set(PASSIVE_WEBUI_PLUGIN_IDS) - set(locks)) - if missing_locks: - raise E1GateError(f"E1 fleet lock 缺少 passive WebUI source: {missing_locks}") - raw_sources = report.get("sources") - if not isinstance(raw_sources, list): - raise E1GateError("passive WebUI report.sources 必须是列表") - source_shas: dict[str, str] = {} - for raw_source in cast(list[object], raw_sources): - source = _report_object(raw_source, "passive WebUI report.sources item") - if source.get("kind") != "plugin": - continue - plugin_id = source.get("id") - if not isinstance(plugin_id, str) or plugin_id not in PASSIVE_WEBUI_PLUGIN_IDS: - raise E1GateError( - f"passive WebUI report 包含非 E1 Citation/Meme source: {plugin_id!r}" - ) - if plugin_id in source_shas: - raise E1GateError(f"passive WebUI report source 重复: {plugin_id}") - resolved_sha = source.get("resolved_sha") - if ( - not isinstance(resolved_sha, str) - or resolved_sha != locks[plugin_id].resolved_sha - ): - raise E1GateError( - f"passive WebUI {plugin_id} source SHA 与 E1 fleet lock 不一致: " - f"expected={locks[plugin_id].resolved_sha} actual={resolved_sha!r}" - ) - source_shas[plugin_id] = resolved_sha - if set(source_shas) != set(PASSIVE_WEBUI_PLUGIN_IDS): - raise E1GateError( - "passive WebUI report 缺少 Citation/Meme source: " - + ", ".join(sorted(set(PASSIVE_WEBUI_PLUGIN_IDS) - set(source_shas))) - ) - return source_shas - - -def _validate_passive_assistant(report: dict[str, object]) -> dict[str, object]: - """验证 WebUI 持久 assistant 同时保留 citation metadata 与 Meme media。""" - - runtime = _report_object(report.get("runtime"), "passive WebUI report.runtime") - if runtime.get("status") != "passed": - raise E1GateError( - f"passive WebUI runtime.status 不是 passed: {runtime.get('status')!r}" - ) - messages = runtime.get("messages") - if not isinstance(messages, list) or len(cast(list[object], messages)) != 2: - raise E1GateError("passive WebUI runtime.messages 必须包含 user + assistant") - message_items = cast(list[object], messages) - _ = _report_object(message_items[0], "passive WebUI user message") - assistant = _report_object(message_items[1], "passive WebUI assistant message") - if assistant.get("role") != "assistant": - raise E1GateError("passive WebUI assistant message role 错误") - if assistant.get("cited_memory_ids") != ["mem_1"]: - raise E1GateError("passive WebUI assistant citation metadata 缺失") - attachment_ids = assistant.get("attachment_ids") - attachments = assistant.get("attachments") - if ( - not isinstance(attachment_ids, list) - or len(attachment_ids) != 1 - or not isinstance(attachment_ids[0], str) - or not isinstance(attachments, list) - or len(attachments) != 1 - or not isinstance(attachments[0], dict) - ): - raise E1GateError("passive WebUI assistant attachment 不符合 fixture") - descriptor = cast(dict[str, object], attachments[0]) - if ( - descriptor.get("artifact_id") != attachment_ids[0] - or descriptor.get("kind") != "image" - or descriptor.get("filename") != "001.png" - or descriptor.get("media_type") != "image/png" - or descriptor.get("url") != f"/api/chat/artifacts/{attachment_ids[0]}" - ): - raise E1GateError("passive WebUI assistant artifact descriptor 漂移") - return { - "cited_memory_ids": assistant["cited_memory_ids"], - "attachment_ids": attachment_ids, - "attachments": attachments, - } - - -def _validate_passive_webui_report( - path: Path, - locks: dict[str, Any], - expected_core: dict[str, object], -) -> dict[str, object]: - """严格验证已完成的 WebUI Gate report,返回可并入 E1 的证据。""" - - if not path.is_file(): - raise E1GateError(f"passive WebUI report 不存在: {path}") - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise E1GateError( - f"passive WebUI report 无法读取: {type(error).__name__}: {error}" - ) from error - report = _report_object(payload, "passive WebUI report") - if report.get("status") != "passed": - raise E1GateError( - f"passive WebUI report.status 不是 passed: {report.get('status')!r}" - ) - if report.get("scenario_profile") != PASSIVE_WEBUI_SCENARIO_PROFILE: - raise E1GateError( - f"passive WebUI scenario_profile 不匹配: {report.get('scenario_profile')!r}" - ) - report_core = _report_object(report.get("core"), "passive WebUI report.core") - if report_core.get("dirty_status") != []: - raise E1GateError("passive WebUI report 不是干净 Core 证据") - for field in ("head", "tree"): - if report_core.get(field) != expected_core.get(field): - raise E1GateError( - f"passive WebUI Core {field} 与当前 E1 不一致: " - f"expected={expected_core.get(field)!r} actual={report_core.get(field)!r}" - ) - source_shas = _validate_passive_sources(report, locks) - runtime = _report_object(report.get("runtime"), "passive WebUI report.runtime") - request = _report_object( - runtime.get("model_request"), "passive WebUI model_request" - ) - citation_index = _report_index( - request.get("citation_index"), "passive WebUI citation_index" - ) - meme_index = _report_index(request.get("meme_index"), "passive WebUI meme_index") - if citation_index >= meme_index: - raise E1GateError( - f"passive WebUI prompt 顺序错误: citation={citation_index} meme={meme_index}" - ) - assistant = _validate_passive_assistant(report) - cleanup = _report_object(report.get("cleanup"), "passive WebUI report.cleanup") - if cleanup.get("residuals") != []: - raise E1GateError( - f"passive WebUI cleanup residuals 非空: {cleanup.get('residuals')!r}" - ) - if ( - cleanup.get("sandbox_removed") is not True - or cleanup.get("source_unchanged") is not True - ): - raise E1GateError( - "passive WebUI cleanup 未同时证明 sandbox_removed/source_unchanged" - ) - return { - "status": "passed", - "report": str(path), - "scenario_profile": report["scenario_profile"], - "source_shas": source_shas, - "prompt_order": {"citation_index": citation_index, "meme_index": meme_index}, - "assistant": assistant, - "cleanup": { - "residuals": [], - "sandbox_removed": True, - "source_unchanged": True, - }, - } - - -def _plugin_dirs(external: dict[str, Path]) -> list[Path]: - """组装真实 PluginManager 的 in-tree 与 exact checkout source roots。""" - - return [BUILTIN_PLUGIN_ROOTS["models"], BUILTIN_PLUGIN_ROOTS["akasha"]] + [ - external[plugin_id] - for plugin_id in E1_EXTERNAL_PLUGIN_IDS - if plugin_id in external - ] - - -async def _open_runtime(workspace: Path, plugin_dirs: list[Path]) -> RuntimeBundle: - """在 disposable workspace 通过普通 PluginManager 启动 Akasha。""" - - workspace.mkdir(parents=True, exist_ok=True) - sessions = SessionManager(workspace) - event_bus = EventBus() - tools = ToolRegistry() - try: - manager = PluginManager( - plugin_dirs, - event_bus=event_bus, - workspace=workspace, - tool_registry=tools, - session_manager=sessions, - installed_cache_root=workspace / "installed-plugins", - ) - manager.bind_activity_host( - ActivityHost( - ( - BackgroundJobActivityAdapter( - manager.snapshot_store, workspace=str(workspace) - ), - ) - ) - ) - await manager.load_all() - except BaseException: - sessions.close() - raise - return RuntimeBundle(workspace, "akasha", sessions, manager) - - -async def _close_runtime(bundle: RuntimeBundle) -> list[str]: - """关闭所有 owner,并返回 cleanup failure 证据。""" - - errors: list[str] = [] - for label, action in ( - ("PluginManager", bundle.manager.terminate_all), - ("SessionManager", bundle.sessions.close), - ): - try: - result = action() - if asyncio.iscoroutine(result): - await result - except Exception as error: - errors.append(f"{label}: {type(error).__name__}: {error}") - return errors - - -def _runtime_identity(bundle: RuntimeBundle) -> dict[str, object]: - """读取 stable snapshot、generation 与 mobile catalog。""" - - snapshot = bundle.manager.current_snapshot - if snapshot is None: - raise E1GateError(f"{bundle.engine_name} runtime 没有 stable snapshot") - active = sorted(item.plugin_id for item in bundle.manager.active_plugins()) - generations = { - plugin_id: generation.source_revision - for plugin_id in active - if (generation := bundle.manager.generation(plugin_id)) is not None - } - return { - "engine": bundle.engine_name, - "snapshot_id": snapshot.snapshot_id, - "active_plugins": active, - "generations": generations, - "mobile_catalog": PluginMobileUiProvider(bundle.manager).catalog(), - "composition_active_plugin_ids": sorted( - snapshot.composition_active_plugin_ids or frozenset() - ), - } - - -async def _probe_boot(bundle: RuntimeBundle) -> dict[str, object]: - """执行 stable lease 与 Akasha bounded mobile query。""" - - identity = _runtime_identity(bundle) - snapshot = bundle.manager.current_snapshot - if snapshot is None: - raise E1GateError("stable snapshot 在 mobile probe 前消失") - before = snapshot.lease_count - async with bundle.manager.snapshot_store.lease() as leased: - lease: dict[str, object] = { - "snapshot_id": leased.snapshot_id, - "during": leased.lease_count, - } - lease["before"] = before - lease["after"] = snapshot.lease_count - identity["stable_lease"] = lease - generation = bundle.manager.generation("akasha") - if generation is None: - raise E1GateError("Akasha generation 缺失") - provider = PluginMobileUiProvider(bundle.manager) - try: - result = await provider.query( - "akasha", - generation.source_revision, - "inspector.recent", - {}, - session_id="e1:mobile", - turn_id="turn:e1:mobile", - ) - except Exception as error: - identity["mobile_query"] = { - "plugin_id": "akasha", - "method": "inspector.recent", - "status": "blocked", - "error": f"{type(error).__name__}: {error}", - } - else: - identity["mobile_query"] = { - "plugin_id": "akasha", - "method": "inspector.recent", - "status": "passed", - "result": result, - } - if snapshot.lease_count != before: - raise E1GateError("stable snapshot lease 未归还") - return identity - - -def _now() -> str: - return datetime.now(UTC).isoformat() - - -def _seed_interaction( - sessions: SessionManager, *, key: str, turn: str, label: str -) -> tuple[str, ...]: - """通过 SessionStore append 一个完整 U+A interaction。""" - - timestamp = _now() - rows = sessions.control_store.persist_session( - key, - created_at=timestamp, - updated_at=timestamp, - metadata={"gate": label}, - messages=[ - { - "role": "user", - "content": f"E1 {label} question", - "timestamp": timestamp, - "extra": {"control_turn_id": turn, "turn_input_ordinal": 0}, - }, - { - "role": "assistant", - "content": f"E1 {label} answer", - "timestamp": timestamp, - "extra": { - "control_turn_id": turn, - "turn_terminal": True, - "turn_input_count": 1, - }, - }, - ], - ) - return tuple(str(row["id"]) for row in rows) - - -def _freeze(value: object) -> object: - if isinstance(value, bytes): - return {"sha256": hashlib.sha256(value).hexdigest(), "length": len(value)} - if value is None or isinstance(value, str | int | float): - return value - return repr(value) - - -def _sqlite_state(path: Path) -> dict[str, object]: - """读取 SQLite integrity、schema 与所有表的行 hash,不写入数据库。""" - - connection = sqlite3.connect(str(path)) - try: - integrity = str(connection.execute("PRAGMA integrity_check").fetchone()[0]) - if integrity != "ok": - raise E1GateError(f"SQLite integrity_check 失败: {path}: {integrity}") - names = [ - str(row[0]) - for row in connection.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" - ) - ] - tables: dict[str, object] = {} - for name in names: - quote = '"' + name.replace('"', '""') + '"' - schema = connection.execute( - "SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (name,) - ).fetchone()[0] - info = list(connection.execute(f"PRAGMA table_info({quote})")) - columns = [str(row[1]) for row in info] - primary = [ - name - for order, name in sorted( - (int(row[5]), str(row[1])) for row in info if int(row[5]) > 0 - ) - ] - without_rowid = "WITHOUT ROWID" in str(schema or "").upper() - rows = ( - connection.execute(f"SELECT * FROM {quote}").fetchall() - if without_rowid - else connection.execute(f"SELECT rowid, * FROM {quote}").fetchall() - ) - values: dict[str, str] = {} - for row in rows: - frozen = [_freeze(item) for item in row] - if without_rowid: - positions = {column: index for index, column in enumerate(columns)} - key_values = ( - [frozen[positions[item]] for item in primary] - if primary - else frozen - ) - key = json.dumps(key_values, ensure_ascii=False, sort_keys=True) - payload = frozen - else: - key = str(frozen[0]) - payload = frozen[1:] - encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True) - values[key] = hashlib.sha256(encoded.encode()).hexdigest() - tables[name] = {"schema": schema, "rows": values} - return {"path": str(path), "integrity": integrity, "tables": tables} - finally: - connection.close() - - -def _sqlite_diff( - before: dict[str, object], after: dict[str, object] -) -> dict[str, object]: - """从两个 SQLite snapshot 生成紧凑完整 write-set。""" - - left = cast(dict[str, object], before["tables"]) - right = cast(dict[str, object], after["tables"]) - inserted: list[str] = [] - deleted: list[str] = [] - updated: list[str] = [] - for table in sorted(set(left) | set(right)): - old = cast( - dict[str, str], - cast(dict[str, object], left.get(table, {"rows": {}}))["rows"], - ) - new = cast( - dict[str, str], - cast(dict[str, object], right.get(table, {"rows": {}}))["rows"], - ) - inserted.extend(f"{table}:{key}" for key in sorted(set(new) - set(old))) - deleted.extend(f"{table}:{key}" for key in sorted(set(old) - set(new))) - updated.extend( - f"{table}:{key}" - for key in sorted(set(old) & set(new)) - if old[key] != new[key] - ) - return { - "inserted": inserted, - "deleted": deleted, - "updated": updated, - "inserted_count": len(inserted), - "deleted_count": len(deleted), - "updated_count": len(updated), - } - - -async def _scenarios( - workspace: Path, plugin_dirs: list[Path], blockers: list[str] -) -> tuple[list[dict[str, object]], dict[str, object]]: - """验证 Akasha 启动、Session 追加与 provide 竞争。""" - - scenarios: list[dict[str, object]] = [] - runtimes: dict[str, RuntimeBundle] = {} - evidence: dict[str, object] = {} - for engine in ("akasha",): - bundle: RuntimeBundle | None = None - try: - bundle = await _open_runtime(workspace / f"runtime-{engine}", plugin_dirs) - runtimes[engine] = bundle - boot = await _probe_boot(bundle) - evidence[engine] = boot - scenarios.append( - {"id": f"runtime_boot_{engine}", "status": "passed", "evidence": boot} - ) - mobile = cast(dict[str, object], boot["mobile_query"]) - if mobile.get("status") == "blocked": - blockers.append( - f"runtime_boot_{engine}.mobile_query: {mobile.get('error', 'unavailable')}" - ) - except Exception as error: - scenarios.append( - { - "id": f"runtime_boot_{engine}", - "status": "failed", - "error": f"{type(error).__name__}: {error}", - } - ) - blockers.append(f"runtime_boot_{engine}: {type(error).__name__}: {error}") - if bundle is not None: - blockers.extend( - f"runtime cleanup: {item}" for item in await _close_runtime(bundle) - ) - _ = runtimes.pop(engine, None) - akasha = runtimes.get("akasha") - if akasha is None: - scenarios.append( - { - "id": "append_only_sessiondb", - "status": "blocked", - "reason": "Akasha runtime 未启动", - } - ) - else: - try: - before = _sqlite_state(akasha.sessions.db_path) - _ = _seed_interaction( - akasha.sessions, - key="e1:append", - turn="turn:e1:append", - label="memory-lifecycle", - ) - after = _sqlite_state(akasha.sessions.db_path) - diff = _sqlite_diff(before, after) - if diff["deleted_count"] != 0: - raise E1GateError("Session append 出现删除 write-set") - scenarios.append( - {"id": "append_only_sessiondb", "status": "passed", "diff": diff} - ) - except Exception as error: - scenarios.append( - { - "id": "append_only_sessiondb", - "status": "failed", - "error": f"{type(error).__name__}: {error}", - } - ) - blockers.append(f"append_only_sessiondb: {type(error).__name__}: {error}") - - competition = CompositionRoot("e1-memory-competition") - - async def first_provider(context: Any) -> None: - _ = await context.provide(EMBEDDING_MEMORY_PLUGIN, object()) - - async def second_provider(context: Any) -> None: - _ = await context.provide(EMBEDDING_MEMORY_PLUGIN, object()) - - await competition.mount(first_provider, name="first-memory") - await competition.mount(second_provider, name="second-memory") - receipt = competition.receipt() - duplicate = next( - ( - incident.message - for incident in receipt.incidents - if "DUPLICATE_SERVICE" in incident.message - ), - None, - ) - if receipt.ready or duplicate is None: - blockers.append("memory_claim_competition: duplicate provider 未阻止启动") - scenarios.append( - { - "id": "memory_claim_competition", - "status": "failed", - "error": "duplicate provider 未阻止启动", - } - ) - else: - scenarios.append( - { - "id": "memory_claim_competition", - "status": "passed", - "error": duplicate, - } - ) - await competition.dispose() - for bundle in runtimes.values(): - blockers.extend( - f"runtime cleanup: {item}" for item in await _close_runtime(bundle) - ) - return scenarios, evidence - - -async def _run_gate( - *, - lock_path: Path, - report_path: Path, - tmp_root: Path | None, - provided_raw: list[str], - offline: bool, - passive_webui_report: Path = DEFAULT_PASSIVE_WEBUI_REPORT, -) -> dict[str, object]: - """执行一次 combined E1 Gate 并持久化 truthful report。""" - - blockers: list[str] = [] - try: - locks = _select_e1_locks(lock_path) - provided = _parse_plugin_roots(provided_raw) - except Exception as error: - locks, provided = {}, {} - blockers.append(f"lock/input: {type(error).__name__}: {error}") - plugin_evidence: list[dict[str, object]] = [] - core = fleet_gate._core_evidence() # pyright: ignore[reportPrivateUsage] - lock_evidence: dict[str, object] = { - "path": str(lock_path), - "sha256": ( - fleet_gate._sha256(lock_path) if lock_path.is_file() else None - ), # pyright: ignore[reportPrivateUsage] - "selected_external_ids": list(E1_EXTERNAL_PLUGIN_IDS), - } - with tempfile.TemporaryDirectory( - dir=tmp_root, prefix="akashic-plugin-v3-e1-" - ) as raw: - workspace = Path(raw) / "workspace" - staging = Path(raw) / "locked-checkouts" - staging.mkdir() - if locks: - external, external_evidence, external_blockers = _resolve_external_roots( - locks, staging, provided, offline=offline - ) - plugin_evidence.extend(external_evidence) - blockers.extend(external_blockers) - else: - external = {} - for plugin_id, root in BUILTIN_PLUGIN_ROOTS.items(): - status = "available" if root.is_dir() else "blocked" - plugin_evidence.append( - { - "id": plugin_id, - "status": status, - "path": str(root), - "source": "in-tree", - } - ) - if status != "available": - blockers.append(f"{plugin_id}: in-tree source missing: {root}") - scenarios, runtime = await _scenarios( - workspace, _plugin_dirs(external), blockers - ) - active: set[str] = set() - for item in runtime.values(): - evidence = cast(dict[str, object], item) - active.update(cast(list[str], evidence["active_plugins"])) - missing_runtime = sorted(set(E1_EXTERNAL_PLUGIN_IDS).difference(active)) - if missing_runtime: - blockers.append( - "required external plugin runtime coverage absent: " - + ", ".join(missing_runtime) - ) - try: - if not locks: - raise E1GateError( - "E1 fleet lock 不可用,无法绑定 passive WebUI source SHA" - ) - passive = _validate_passive_webui_report( - passive_webui_report, - locks, - core, - ) - except E1GateError as error: - scenarios.append( - { - "id": "passive_prompt_metadata_media", - "status": "blocked", - "reason": str(error), - "report": str(passive_webui_report), - } - ) - blockers.append(f"passive_prompt_metadata_media: {error}") - else: - scenarios.append({"id": "passive_prompt_metadata_media", **passive}) - report: dict[str, object] = { - "status": ( - "passed" - if not blockers - and all(item["status"] == "passed" for item in scenarios) - else "blocked" - ), - "phase": "e1", - "gate_version": 1, - "checked_at": datetime.now(UTC).isoformat(), - "disposable_workspace": str(workspace), - "workspace_persisted": False, - "core": core, - "lock": lock_evidence, - "plugins": plugin_evidence, - "runtime": runtime, - "scenarios": scenarios, - "blockers": sorted(set(blockers)), - } - report_path.parent.mkdir(parents=True, exist_ok=True) - _ = report_path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - return report - - -def main() -> int: - """运行 E1 Gate;blocked/failed 证据返回非零。""" - - args = _parse_args() - report = asyncio.run( - _run_gate( - lock_path=args.lock.resolve(), - report_path=args.report.resolve(), - tmp_root=None if args.tmp_root is None else args.tmp_root.resolve(), - provided_raw=cast(list[str], args.plugin_root), - offline=bool(args.offline), - passive_webui_report=args.passive_webui_report.resolve(), - ) - ) - print(f"plugin v3 E1 Gate {report['status']}: {args.report.resolve()}") - for blocker in cast(list[str], report["blockers"]): - print(f"- {blocker}", file=sys.stderr) - return 0 if report["status"] == "passed" else 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/docker/debug/plugin_v3_e2_gate.py b/docker/debug/plugin_v3_e2_gate.py deleted file mode 100644 index b3402d56c..000000000 --- a/docker/debug/plugin_v3_e2_gate.py +++ /dev/null @@ -1,1486 +0,0 @@ -"""Concentrated disposable-workspace E2 Gate for the locked v3 plugin fleet. - -The gate deliberately keeps the production ``PluginManager`` and its typed -runtime hosts in the loop. It never changes the checkout, never uses formal -credentials, and records ``blocked`` when the supplied Python runtime cannot -start the locked recording backends. -""" - -from __future__ import annotations - -import argparse -import asyncio -import hashlib -import json -import os -import re -import shlex -import signal -import shutil -import subprocess -import sys -import tempfile -import textwrap -import urllib.request -from collections.abc import Awaitable, Callable, Mapping -from dataclasses import asdict, dataclass -from datetime import UTC, datetime -from pathlib import Path -from typing import Any, cast - -ROOT = Path(__file__).resolve().parents[2] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from agent.plugins.artifacts import ( # noqa: E402 - ArtifactPointer, - read_pointers, - resolve_pointer, - write_pointers, -) -from agent.plugins.manager import PluginManager # noqa: E402 -from agent.plugins.generation_activity_host import ActivityHost # noqa: E402 -from agent.plugins.generation_job_host import BackgroundJobActivityAdapter # noqa: E402 -from agent.plugins.manifest import write_plugin_manifest # noqa: E402 -from agent.plugins.snapshot import ( # noqa: E402 - bind_runtime_snapshot, - reset_runtime_snapshot, -) -from agent.plugins.static_manifest import load_static_plugin_manifest # noqa: E402 -from agent.tools.events import ( # noqa: E402 - ToolExecutionRequest, - ToolExecutionResult, -) -from agent.tools.executor import ToolExecutor # noqa: E402 -from bus.event_bus import EventBus # noqa: E402 - -DEFAULT_LOCK = ROOT / "docker" / "debug" / "plugin-v3-fleet.lock.json" -DEFAULT_REPORT = ROOT / "docker" / "debug" / "reports" / "plugin-v3-e2" / "gate.json" -GATE_VERSION = 1 -COMMIT_PATTERN = re.compile(r"[0-9a-f]{40}") - -EXPECTED_PLUGIN_IDS = ( - "shell_restore", - "shell_safety", - "calendar-mcp", - "feed-mcp", - "fitbit-mcp", - "steam-mcp", -) -SHELL_PLUGIN_IDS = EXPECTED_PLUGIN_IDS[:2] -MCP_PLUGIN_IDS = EXPECTED_PLUGIN_IDS[2:] -INSTALLED_NAMES = { - "calendar-mcp": "calendar", - "feed-mcp": "feed", - "fitbit-mcp": "fitbit", - "steam-mcp": "steam", -} -EXPECTED_LISTENERS = ( - "transform:tool.input.prepare[akashic.tool-input.v1]:shell_restore", - "serial:tool.execution.authorize[bail=akashic.tool-deny-reason.v1]:shell_safety", -) -SCENARIO_PROFILE = "plugin-v3-e2-shell-v1" -READONLY_PROBES: dict[str, tuple[str, ...]] = { - "fitbit-mcp": ("get_sleep_context",), - "steam-mcp": ("get_steam_context",), -} -FORMAL_PORTS = {"calendar-mcp": 18000, "fitbit-mcp": 18765} -REQUIRED_IMPORTS = { - "calendar-mcp": ( - "mcp", - "fastapi", - "uvicorn", - "dotenv", - "dateutil", - "google.oauth2.credentials", - "google.auth.transport.requests", - "googleapiclient.discovery", - "google_auth_oauthlib.flow", - ), - "feed-mcp": ("mcp",), - "fitbit-mcp": ("mcp", "fastapi", "uvicorn", "requests"), - "steam-mcp": ("mcp",), -} - -ToolInvoker = Callable[[str, dict[str, Any]], Awaitable[Any]] - - -class GateBlocked(RuntimeError): - """Indicate an environment or runtime prerequisite that was not met.""" - - -@dataclass(frozen=True, slots=True) -class PluginLock: - id: str - repository: str - requested_ref: str - resolved_sha: str - change_source_pr_head: str - - -@dataclass(frozen=True, slots=True) -class PluginEvidence: - id: str - repository: str - requested_ref: str - resolved_sha: str - change_source_pr_head: str - tree: str - - -@dataclass(frozen=True, slots=True) -class ScenarioCase: - id: str - session: str - command: str - expected_status: str - expected_invoked: bool - - -@dataclass(frozen=True, slots=True) -class ScenarioEvidence: - id: str - status: str - final_command: str - invoked: bool - exit_code: int | None - - -@dataclass(frozen=True, slots=True) -class RuntimeEvidence: - id: str - plugin_id: str - generation_id: str - mode: str - state: str - mcp_tools: tuple[str, ...] - probes: tuple[dict[str, object], ...] - process_endpoints: tuple[dict[str, object], ...] - candidate_workspace: str - formal_data_before: str - formal_data_after: str - cleanup: dict[str, object] - stable_pointer: str - latest_pointer: str - - -@dataclass(frozen=True, slots=True) -class CleanupEvidence: - shell_generation_ids: tuple[str, ...] - retained_runtime_failures: tuple[str, ...] - cleanup_failures: tuple[str, ...] - listeners: tuple[str, ...] - effects: tuple[str, ...] - - -SCENARIO_CATALOG = ( - ScenarioCase("plain-rm", "plain", "rm /tmp/plain.txt", "success", True), - ScenarioCase( - "sudo-cluster", - "cluster", - "sudo -nE rm /tmp/cluster.txt", - "success", - True, - ), - ScenarioCase( - "sudo-preserve-env", - "env", - "sudo -n --preserve-env=HOME rm /tmp/env.txt", - "success", - True, - ), - ScenarioCase( - "sudo-mode-denied", - "mode", - "sudo -n -s rm /tmp/mode.txt", - "denied", - False, - ), - ScenarioCase("repeat-1", "repeat", "rm /tmp/repeat.txt", "success", True), - ScenarioCase("repeat-2", "repeat", "rm /tmp/repeat.txt", "success", True), - ScenarioCase("repeat-3", "repeat", "rm /tmp/repeat.txt", "success", True), -) - - -def _required_string(item: Mapping[str, object], name: str) -> str: - value = item.get(name) - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"锁字段必须是非空字符串: {name}") - return value - - -def _parse_plugin_lock(raw: object) -> PluginLock: - """Parse one lock entry and require one immutable commit identity.""" - - # 1. Reject fields that are not part of the fleet lock contract. - expected = { - "id", - "repository", - "requested_ref", - "resolved_sha", - "change_source_pr_head", - } - if not isinstance(raw, dict) or set(raw) != expected: - raise ValueError(f"v3 fleet lock entry 字段无效: {raw!r}") - - # 2. Freeze the repository and all three revision claims. - item = cast(dict[str, object], raw) - values = {key: _required_string(item, key) for key in expected} - repository = values["repository"] - if not repository.startswith("https://github.com/"): - raise ValueError(f"插件仓库必须是 GitHub HTTPS 地址: {repository}") - revisions = tuple( - values[key] - for key in ("requested_ref", "resolved_sha", "change_source_pr_head") - ) - if any(COMMIT_PATTERN.fullmatch(value) is None for value in revisions): - raise ValueError(f"插件 revision 必须是完整 40-hex SHA: {values['id']}") - if len(set(revisions)) != 1: - raise ValueError(f"插件三个 revision 必须完全一致: {values['id']}") - return PluginLock( - id=values["id"], - repository=repository, - requested_ref=values["requested_ref"], - resolved_sha=values["resolved_sha"], - change_source_pr_head=values["change_source_pr_head"], - ) - - -def _load_lock(path: Path) -> tuple[PluginLock, ...]: - """Load the seven E2 entries from the shared fleet lock.""" - - # 1. Validate the shared document without accepting a missing or duplicate id. - raw = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(raw, dict) or set(raw) != {"schema_version", "plugins"}: - raise ValueError("v3 fleet lock 根结构无效") - if raw["schema_version"] != 1: - raise ValueError(f"不支持的 v3 fleet lock 版本: {raw['schema_version']!r}") - entries = raw["plugins"] - if not isinstance(entries, list): - raise ValueError("v3 fleet lock plugins 必须是列表") - parsed = tuple(_parse_plugin_lock(item) for item in entries) - by_id: dict[str, PluginLock] = {} - for item in parsed: - if item.id in by_id: - raise ValueError(f"v3 fleet lock 存在重复插件: {item.id}") - by_id[item.id] = item - - # 2. Select the exact E2 contract in its contract order. - missing = tuple( - plugin_id for plugin_id in EXPECTED_PLUGIN_IDS if plugin_id not in by_id - ) - if missing: - raise ValueError(f"v3 E2 lock 缺少插件: {', '.join(missing)}") - return tuple(by_id[plugin_id] for plugin_id in EXPECTED_PLUGIN_IDS) - - -def _run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]: - """Run one checked local command and preserve stderr for the caller.""" - - return subprocess.run( - command, - cwd=cwd, - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - - -def _git_output(cwd: Path, *args: str) -> str: - return _run(("git", *args), cwd=cwd).stdout.strip() - - -def _checkout_locked_plugin(lock: PluginLock, checkout: Path) -> PluginEvidence: - """Fetch one exact public Git object into a new disposable checkout.""" - - # 1. Create an isolated repository and fetch only the locked object. - checkout.parent.mkdir(parents=True, exist_ok=True) - _run(("git", "init", "--quiet", str(checkout)), cwd=ROOT) - _run(("git", "remote", "add", "origin", lock.repository), cwd=checkout) - _run( - ("git", "fetch", "--quiet", "--depth=1", "origin", lock.resolved_sha), - cwd=checkout, - ) - _run(("git", "checkout", "--quiet", "--detach", "FETCH_HEAD"), cwd=checkout) - - # 2. Verify both commit and working-tree identity before handing it to Core. - if _git_output(checkout, "rev-parse", "HEAD") != lock.resolved_sha: - raise RuntimeError(f"插件检出提交与锁不一致: {lock.id}") - if _git_output(checkout, "status", "--porcelain"): - raise RuntimeError(f"插件检出后工作树不干净: {lock.id}") - return PluginEvidence( - id=lock.id, - repository=lock.repository, - requested_ref=lock.requested_ref, - resolved_sha=lock.resolved_sha, - change_source_pr_head=lock.change_source_pr_head, - tree=_git_output(checkout, "rev-parse", "HEAD^{tree}"), - ) - - -def _sha256(path: Path) -> str: - return hashlib.sha256(path.read_bytes()).hexdigest() - - -def _scenario_catalog_sha256() -> str: - encoded = json.dumps( - [asdict(item) for item in SCENARIO_CATALOG], - ensure_ascii=False, - separators=(",", ":"), - sort_keys=True, - ).encode() - return hashlib.sha256(encoded).hexdigest() - - -def _runtime_interpreter(path: Path) -> Path: - """Validate one supplied interpreter without mutating its environment.""" - - resolved = path.expanduser().resolve(strict=True) - if not resolved.is_file() or not os.access(resolved, os.X_OK): - raise GateBlocked(f"runtime Python 不可执行: {resolved}") - return resolved - - -def _check_imports(runtime_python: Path, plugin_ids: tuple[str, ...]) -> None: - """Check imports required by the locked candidate recording processes.""" - - # 1. Query the supplied interpreter, rather than this Gate's interpreter. - modules = tuple( - dict.fromkeys( - module for plugin_id in plugin_ids for module in REQUIRED_IMPORTS[plugin_id] - ) - ) - script = textwrap.dedent(""" - import importlib.util - import json - import sys - missing = [name for name in sys.argv[1:] if importlib.util.find_spec(name) is None] - print(json.dumps(missing)) - """) - result = subprocess.run( - (str(runtime_python), "-c", script, *modules), - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - missing = json.loads(result.stdout) - if missing: - raise GateBlocked( - "recording runtime 依赖未安装;请使用声明 requirements staging 后重试: " - + ", ".join(str(item) for item in missing) - ) - - -def _create_runtime_stage( - bootstrap_python: Path, - sandbox: Path, - checkouts: Mapping[str, Path], -) -> tuple[Path, Path, tuple[dict[str, object], ...]]: - """Build one disposable runtime from every locked manifest requirement.""" - - # 1. Inherit the verified Core environment without mutating it. - stage = sandbox / "runtime-python" - created = subprocess.run( - ( - str(bootstrap_python), - "-m", - "venv", - "--system-site-packages", - str(stage), - ), - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if created.returncode != 0: - raise GateBlocked( - "E2 recording runtime venv 创建失败: " + created.stderr.strip() - ) - runtime_python = stage / ("Scripts/python.exe" if os.name == "nt" else "bin/python") - - # 2. Install exactly the requirements declared by the locked artifacts. - evidence: list[dict[str, object]] = [] - for plugin_id in MCP_PLUGIN_IDS: - manifest = load_static_plugin_manifest(checkouts[plugin_id]) - for runtime in manifest.python: - requirements = checkouts[plugin_id] / runtime.requirements - installed = subprocess.run( - ( - str(runtime_python), - "-m", - "pip", - "install", - "--disable-pip-version-check", - "-r", - str(requirements), - ), - cwd=checkouts[plugin_id], - check=False, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - if installed.returncode != 0: - raise GateBlocked( - f"{plugin_id} requirements staging 失败: " - + installed.stderr.strip() - ) - evidence.append( - { - "plugin_id": plugin_id, - "requirements": runtime.requirements, - "requirements_sha256": _sha256(requirements), - } - ) - _check_imports(runtime_python, MCP_PLUGIN_IDS) - return stage, runtime_python, tuple(evidence) - - -def _copy_source_to_artifact(source: Path, target: Path) -> None: - """Copy a locked source tree while excluding VCS and generated files.""" - - shutil.copytree( - source, - target, - ignore=shutil.ignore_patterns( - ".git", - ".venv", - "__pycache__", - ".pytest_cache", - ".mypy_cache", - ".ruff_cache", - "node_modules", - ), - ) - - -def _stage_candidate_artifact( - source: Path, - plugin_id: str, - cache_root: Path, - runtime_stage: Path, -) -> Path: - """Stage stable and latest copies for one installed candidate transaction.""" - - # 1. Materialize exact source copies under the disposable installed cache. - manifest = load_static_plugin_manifest(source) - plugin_base = cache_root / "github" / manifest.name - stable = plugin_base / ".artifacts" / "stable" - latest = plugin_base / ".artifacts" / "latest" - _copy_source_to_artifact(source, stable) - _copy_source_to_artifact(source, latest) - - # 2. Bind every declared Python runtime to the caller-supplied interpreter. - for artifact in (stable, latest): - for runtime in manifest.python: - runtime_root = (artifact / runtime.runtime_root).resolve(strict=False) - runtime_root.mkdir(parents=True, exist_ok=True) - link = runtime_root / ".venv" - if link.exists() or link.is_symlink(): - raise RuntimeError(f"artifact runtime staging target 已存在: {link}") - link.symlink_to(runtime_stage, target_is_directory=True) - plugin_base.mkdir(parents=True, exist_ok=True) - _ = write_pointers( - plugin_base, - stable=ArtifactPointer(".artifacts/stable"), - latest=ArtifactPointer(".artifacts/latest"), - ) - if not plugin_id.endswith("-mcp"): - raise ValueError(f"installed candidate id 与 E2 plugin id 不一致: {plugin_id}") - return plugin_base - - -def _prepare_external_candidates( - checkouts: Mapping[str, Path], - cache_root: Path, - runtime_stage: Path, -) -> dict[str, Path]: - """Stage all four MCP candidates without loading their formal generations.""" - - cache_root.mkdir(parents=True, exist_ok=True) - bases: dict[str, Path] = {} - for plugin_id in MCP_PLUGIN_IDS: - bases[plugin_id] = _stage_candidate_artifact( - checkouts[plugin_id], - plugin_id, - cache_root, - runtime_stage, - ) - entries = {f"{INSTALLED_NAMES[item]}@github": True for item in MCP_PLUGIN_IDS} - _ = write_plugin_manifest(entries, plugins_home=cache_root.parent) - return bases - - -def _assert_exact_pointer_pair( - source: Path, - plugin_base: Path, - *, - context: str, - require_converged: bool = False, -) -> tuple[str, str]: - """Verify both durable pointers resolve to artifacts of the locked source.""" - - # 1. Read the Manager-owned pointer state; never infer a candidate from a directory name. - source_manifest = load_static_plugin_manifest(source) - pointers = read_pointers(plugin_base) - if pointers is None: - raise RuntimeError(f"{context} 缺少 artifact pointer: {plugin_base}") - if pointers.stable.path is None or pointers.latest.path is None: - raise RuntimeError(f"{context} stable/latest pointer 不能为空: {plugin_base}") - if require_converged and pointers.stable != pointers.latest: - raise RuntimeError( - f"{context} stable/latest pointer 未收敛: " - f"stable={pointers.stable.path} latest={pointers.latest.path}" - ) - - # 2. Resolve through the canonical artifact validator and compare static identity. - for selector, pointer in ( - ("stable", pointers.stable), - ("latest", pointers.latest), - ): - artifact = resolve_pointer(plugin_base, pointer) - if artifact is None: - raise RuntimeError(f"{context} {selector} pointer 解析为空") - artifact_manifest = load_static_plugin_manifest(artifact) - if artifact_manifest.identity_digest != source_manifest.identity_digest: - raise RuntimeError( - f"{context} {selector} artifact manifest identity 漂移: " - f"expected={source_manifest.identity_digest} " - f"actual={artifact_manifest.identity_digest}" - ) - return pointers.stable.path, pointers.latest.path - - -def _rebuild_exact_latest_candidate( - source: Path, - plugin_base: Path, - runtime_stage: Path, -) -> tuple[str, str]: - """Rebuild Steam's disposable latest artifact after a discarded probe.""" - - # 1. Preserve the exact stable artifact and reject a missing or drifted base. - manifest = load_static_plugin_manifest(source) - pointers = read_pointers(plugin_base) - if pointers is None or pointers.stable.path is None: - raise RuntimeError(f"重建 latest 前缺少 stable pointer: {plugin_base}") - stable = resolve_pointer(plugin_base, pointers.stable) - if stable is None: - raise RuntimeError(f"重建 latest 前 stable pointer 解析为空: {plugin_base}") - stable_manifest = load_static_plugin_manifest(stable) - if stable_manifest.identity_digest != manifest.identity_digest: - raise RuntimeError( - f"重建 latest 前 stable artifact identity 漂移: {plugin_base}" - ) - - # 2. Materialize a fresh exact candidate and bind every declared runtime. - candidate_pointer = ".artifacts/latest-e2-retry" - candidate = plugin_base / candidate_pointer - if candidate.exists() or candidate.is_symlink(): - raise RuntimeError(f"重建 latest 目标已存在: {candidate}") - _copy_source_to_artifact(source, candidate) - for runtime in manifest.python: - runtime_root = (candidate / runtime.runtime_root).resolve(strict=False) - runtime_root.mkdir(parents=True, exist_ok=True) - link = runtime_root / ".venv" - if link.exists() or link.is_symlink(): - raise RuntimeError(f"重建 latest runtime staging target 已存在: {link}") - link.symlink_to(runtime_stage, target_is_directory=True) - _ = write_pointers( - plugin_base, - stable=pointers.stable, - latest=ArtifactPointer(candidate_pointer), - ) - stable_path, latest_path = _assert_exact_pointer_pair( - source, - plugin_base, - context="Steam in-process failure candidate 重建后", - ) - if stable_path == latest_path: - raise RuntimeError( - "Steam in-process failure candidate 未形成独立 latest pointer" - ) - return stable_path, latest_path - - -def _write_formal_steam_config(workspace: Path) -> Path: - """Seed disposable formal Steam data before stable runtime admission.""" - - data_root = workspace / "plugin-data" / "steam-github" - data_root.mkdir(parents=True, exist_ok=True) - config = data_root / "steam_mcp_config.json" - config.write_text( - json.dumps( - { - "steam_api_key": "test-only", - "steam_id": "76561198000000000", - "snapshot_interval_seconds": 3600, - } - ), - encoding="utf-8", - ) - return config - - -def _make_fake_sudo(bin_dir: Path) -> None: - """Install a local non-privileged sudo shim for disposable shell commands.""" - - # 1. The shim only strips the tested non-interactive flags. - script = """#!/bin/sh -set -eu -while [ "$#" -gt 0 ]; do - case "$1" in - -n|-nE|-E|--non-interactive|--preserve-env|--preserve-env=*) shift ;; - *) break ;; - esac -done -exec "$@" -""" - bin_dir.mkdir(parents=True, exist_ok=True) - path = bin_dir / "sudo" - path.write_text(script, encoding="utf-8") - path.chmod(0o755) - - -def _shell_command_for_case(case: ScenarioCase, target_root: Path) -> tuple[str, Path]: - stem = case.command.rsplit("/", 1)[-1] - target = target_root / stem - return case.command.replace(f"/tmp/{stem}", str(target)), target - - -def _assert_shell_scenario( - case: ScenarioCase, - result: ToolExecutionResult, - final_command: str, - restore_dir: Path, -) -> None: - """Assert locked status, transformed command and real file movement.""" - - if result.status != case.expected_status: - raise RuntimeError(f"场景 {case.id} 状态错误: {result.status} {result.output}") - if case.expected_status == "success": - tokens = shlex.split(final_command) - if "mv" not in tuple(Path(item).name for item in tokens): - raise RuntimeError(f"场景 {case.id} 未执行 rm -> mv: {final_command}") - if str(restore_dir) not in tokens: - raise RuntimeError(f"场景 {case.id} 未指向还原目录: {final_command}") - elif case.id == "sudo-mode-denied": - if "普通命令执行" not in str(result.output): - raise RuntimeError(f"场景 {case.id} 未由 Safety 拒绝: {result.output}") - - -async def _run_shell_scenarios( - manager: PluginManager, - sandbox: Path, -) -> tuple[tuple[str, ...], tuple[ScenarioEvidence, ...], list[dict[str, object]]]: - """Run the exact Shell Restore/Safety/Loop Guard catalog through ToolExecutor.""" - - # 1. Freeze the formal Root topology and bind one real runtime lease. - snapshot = manager.current_snapshot - if snapshot is None or snapshot.composition_root is None: - raise RuntimeError("正式 snapshot 缺少 v3 CompositionRoot") - listeners = snapshot.composition_root.topology_view().listeners - if listeners != EXPECTED_LISTENERS: - raise RuntimeError(f"v3 Shell listener 顺序不符合锁定合同: {listeners}") - restore = manager.generation("shell_restore") - if restore is None: - raise RuntimeError("正式 snapshot 缺少 shell_restore generation") - restore_dir = restore.data_dir / "restore" - target_root = sandbox / "shell-targets" - fake_bin = sandbox / "fake-bin" - _make_fake_sudo(fake_bin) - invocations: list[dict[str, object]] = [] - executor = ToolExecutor() - - async def invoke(tool_name: str, arguments: dict[str, Any]) -> object: - if tool_name != "shell": - raise RuntimeError(f"E2 shell invoker 收到未知工具: {tool_name}") - command = str(arguments.get("command", "")) - environment = dict(os.environ) - environment["PATH"] = f"{fake_bin}:{environment.get('PATH', '')}" - completed = await asyncio.to_thread( - subprocess.run, - command, - shell=True, - cwd=target_root, - env=environment, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - timeout=5, - check=False, - ) - record = { - "tool_name": tool_name, - "arguments": dict(arguments), - "returncode": completed.returncode, - "stdout": completed.stdout, - "stderr": completed.stderr, - } - invocations.append(record) - if completed.returncode != 0: - raise RuntimeError( - f"真实 shell 返回 {completed.returncode}: {completed.stderr}" - ) - return completed.stdout - - target_root.mkdir(parents=True, exist_ok=True) - lease = manager.snapshot_store.lease() - token = bind_runtime_snapshot(lease) - evidence: list[ScenarioEvidence] = [] - try: - for index, case in enumerate(SCENARIO_CATALOG): - command, target = _shell_command_for_case(case, target_root) - if case.expected_invoked: - target.write_text(case.id, encoding="utf-8") - before = len(invocations) - result = await executor.execute( - ToolExecutionRequest( - call_id=f"e2-shell-{index}", - tool_name="shell", - arguments={"command": command}, - source="passive", - session_key=case.session, - ), - invoke, - ) - invoked = len(invocations) == before + 1 - if invoked != case.expected_invoked: - raise RuntimeError( - f"场景 {case.id} invoker 状态错误: expected={case.expected_invoked} actual={invoked}" - ) - final_command = str(result.final_arguments.get("command", "")) - _assert_shell_scenario(case, result, final_command, restore_dir) - if ( - case.expected_status == "success" - and not (restore_dir / target.name).is_file() - ): - raise RuntimeError( - f"场景 {case.id} 未观察到真实文件进入 restore: {target}" - ) - exit_code: int | None = None - if invoked: - raw_exit_code = invocations[-1].get("returncode") - if not isinstance(raw_exit_code, int): - raise RuntimeError(f"场景 {case.id} 缺少真实进程返回码") - exit_code = raw_exit_code - evidence.append( - ScenarioEvidence( - id=case.id, - status=result.status, - final_command=final_command, - invoked=invoked, - exit_code=exit_code, - ) - ) - finally: - reset_runtime_snapshot(token) - await lease.release() - if len(invocations) != 5: - raise RuntimeError(f"Shell 真实 invoker 调用次数错误: {len(invocations)}") - return listeners, tuple(evidence), invocations - - -def _json_output(output: str) -> object: - try: - return json.loads(output) - except json.JSONDecodeError as error: - raise RuntimeError( - f"recording MCP 返回非 JSON typed payload: {output!r}" - ) from error - - -def _assert_recording_payload(plugin_id: str, tool_name: str, payload: object) -> None: - """Require the known recording payload shape for each read-only tool.""" - - if not isinstance(payload, dict): - raise RuntimeError(f"{plugin_id}:{tool_name} payload 不是 object: {payload!r}") - if plugin_id == "steam-mcp": - items = payload.get("items") - if not isinstance(items, list) or not items or not isinstance(items[0], dict): - raise RuntimeError(f"{plugin_id}:{tool_name} recording items 缺失") - if items[0].get("recording") is not True: - raise RuntimeError(f"{plugin_id}:{tool_name} 未返回 recording=true") - return - if payload.get("status") != "empty": - raise RuntimeError(f"{plugin_id}:{tool_name} 未返回 typed empty: {payload!r}") - - -def _readiness(endpoint_url: str) -> int: - request = urllib.request.Request(endpoint_url, method="GET") - with urllib.request.urlopen(request, timeout=5) as response: - return int(response.status) - - -async def _probe_candidate( - manager: PluginManager, - plugin_id: str, - bases: Mapping[str, Path], - sources: Mapping[str, Path], -) -> RuntimeEvidence: - """Publish one latest candidate, exercise recording routes, then discard it.""" - - installed_id = f"{INSTALLED_NAMES[plugin_id]}@github" - _assert_exact_pointer_pair( - sources[plugin_id], - bases[plugin_id], - context=f"{plugin_id} normal probe 前", - ) - formal_data = ( - manager._workspace / "plugin-data" / f"{INSTALLED_NAMES[plugin_id]}-github" - ) - before = _sha256_tree(formal_data) - prepared = await manager.prepare_candidate(installed_id) - if prepared is None or prepared.runtime_snapshot is None: - raise GateBlocked(f"{plugin_id} prepare_candidate 未生成 typed snapshot") - generation_id = prepared.generation_id - candidate_workspace = prepared.validation_workspace - if candidate_workspace is None: - raise RuntimeError(f"{plugin_id} candidate 缺少 validation workspace") - result = await manager.publish_prepared(installed_id) - if result.get("publication_state") != "latest_ready": - raise RuntimeError(f"{plugin_id} candidate 未进入 latest_ready: {result}") - runtime = manager.composition_generation_host.get(generation_id) - if runtime is None or runtime.mode != "candidate": - raise RuntimeError(f"{plugin_id} 未观察到 candidate CompositionRuntime") - if runtime.mcp is None: - raise RuntimeError(f"{plugin_id} candidate 缺少 MCP generation") - server_name = INSTALLED_NAMES[plugin_id] - server = runtime.mcp.server(server_name) - expected_tools = READONLY_PROBES.get(plugin_id, ()) - missing_tools = set(expected_tools) - set(server.tool_names) - if missing_tools: - raise RuntimeError( - f"{plugin_id} candidate 缺少只读探针: {sorted(missing_tools)}" - ) - candidate_state = runtime.mcp.state - candidate_tools = tuple(server.tool_names) - route = server.route() - probes: list[dict[str, object]] = [] - for tool_name in expected_tools: - call = await route.call(tool_name, {}) - if not call.success: - raise RuntimeError(f"{plugin_id}:{tool_name} MCP tool_error: {call.output}") - payload = _json_output(call.output) - _assert_recording_payload(plugin_id, tool_name, payload) - probes.append({"tool": tool_name, "status": call.status, "payload": payload}) - process_endpoints: list[dict[str, object]] = [] - if runtime.processes is not None: - for name, endpoint in runtime.processes.endpoints.items(): - status = _readiness(endpoint.readiness_url) - if status != 200: - raise RuntimeError(f"{plugin_id}:{name} readiness 非 200: {status}") - formal_port = FORMAL_PORTS.get(plugin_id) - if formal_port is not None and endpoint.port == formal_port: - raise RuntimeError( - f"{plugin_id}:{name} candidate 占用 formal port {formal_port}" - ) - process_endpoints.append( - { - "name": name, - "port": endpoint.port, - "readiness_url": endpoint.readiness_url, - "status": status, - "epoch": endpoint.epoch, - } - ) - after_probe = _sha256_tree(formal_data) - if before != after_probe: - raise RuntimeError(f"{plugin_id} candidate recording 改写 formal plugin-data") - - cleanup = await manager.drop_candidate(installed_id) - stable_pointer, latest_pointer = _assert_exact_pointer_pair( - sources[plugin_id], - bases[plugin_id], - context=f"{plugin_id} normal probe 后", - require_converged=True, - ) - retained_runtime = manager.composition_generation_host.get(generation_id) - retained_failure = manager.composition_generation_host.failure(generation_id) - if retained_runtime is not None or retained_failure is not None: - raise RuntimeError(f"{plugin_id} discard 后仍保留 runtime owner") - if candidate_workspace.parent.exists(): - raise RuntimeError( - f"{plugin_id} candidate workspace 未清理: {candidate_workspace.parent}" - ) - return RuntimeEvidence( - id=plugin_id, - plugin_id=installed_id, - generation_id=generation_id, - mode=runtime.mode, - state=candidate_state, - mcp_tools=candidate_tools, - probes=tuple(probes), - process_endpoints=tuple(process_endpoints), - candidate_workspace=str(candidate_workspace), - formal_data_before=before, - formal_data_after=after_probe, - cleanup=cast(dict[str, object], cleanup), - stable_pointer=stable_pointer, - latest_pointer=latest_pointer, - ) - - -def _sha256_tree(path: Path) -> str: - """Hash one disposable data tree without inventing missing state.""" - - digest = hashlib.sha256() - if not path.exists(): - return digest.hexdigest() - if not path.is_dir(): - raise RuntimeError(f"plugin-data 不是目录: {path}") - for item in sorted(path.rglob("*")): - relative = item.relative_to(path).as_posix() - digest.update(relative.encode()) - if item.is_file(): - digest.update(item.read_bytes()) - return digest.hexdigest() - - -async def _run_in_process_failure( - manager: PluginManager, - bases: Mapping[str, Path], - sources: Mapping[str, Path], - runtime_stage: Path, -) -> dict[str, object]: - """Inject one invariant failure and verify pointer/runtime rollback in-process.""" - - plugin_id = "steam-mcp" - installed_id = f"{INSTALLED_NAMES[plugin_id]}@github" - _rebuild_exact_latest_candidate( - sources[plugin_id], - bases[plugin_id], - runtime_stage, - ) - _assert_exact_pointer_pair( - sources[plugin_id], - bases[plugin_id], - context="Steam in-process failure 前", - ) - prepared = await manager.prepare_candidate(installed_id) - if prepared is None: - raise GateBlocked("in-process failure probe 无法准备 Steam candidate") - generation_id = prepared.generation_id - manager_any = cast(Any, manager) - original = manager_any._post_publish_invariants - - async def fail_invariant(*_args: object, **_kwargs: object) -> None: - raise RuntimeError("e2 forced in-process invariant failure") - - manager_any._post_publish_invariants = fail_invariant - observed: str | None = None - try: - try: - await manager.publish_prepared(installed_id) - except RuntimeError as error: - observed = str(error) - else: - raise RuntimeError("in-process failure injection 未暴露异常") - finally: - manager_any._post_publish_invariants = original - if observed is None or ( - "post-publish" not in observed and "invariant" not in observed - ): - raise RuntimeError(f"in-process failure 语义不明确: {observed}") - if manager.prepared_generation(installed_id) is not None: - raise RuntimeError("in-process failure 后 prepared generation 未清理") - if manager.composition_generation_host.get(generation_id) is not None: - raise RuntimeError("in-process failure 后 candidate runtime 未清理") - stable_pointer, latest_pointer = _assert_exact_pointer_pair( - sources[plugin_id], - bases[plugin_id], - context="Steam in-process failure 后", - require_converged=True, - ) - return { - "id": "in-process-failure", - "status": "passed", - "plugin_id": installed_id, - "generation_id": generation_id, - "error": observed, - "pointer": {"stable": stable_pointer, "latest": latest_pointer}, - } - - -def _boot_process_ids(boot_id: str) -> tuple[int, ...]: - """Return Linux process identities carrying one exact Core boot token.""" - - expected = f"AKASHIC_BOOT_ID={boot_id}".encode() - process_ids: list[int] = [] - for entry in Path("/proc").iterdir(): - if not entry.name.isdigit(): - continue - try: - environ = (entry / "environ").read_bytes().split(b"\0") - except OSError: - continue - if expected in environ: - process_ids.append(int(entry.name)) - return tuple(sorted(process_ids)) - - -async def _run_core_process_crash( - checkouts: Mapping[str, Path], - sandbox: Path, - runtime_python: Path, -) -> dict[str, object]: - """SIGKILL a child Core after candidate start and verify durable recovery.""" - - # 1. Use a separate disposable manager/cache so the active Gate manager is untouched. - crash_root = sandbox / "core-crash" - providers = crash_root / "providers" - cache_root = crash_root / "plugin-home" / "cache" - workspace = crash_root / "workspace" - evidence_path = crash_root / "child-evidence.json" - providers.mkdir(parents=True) - for plugin_id in SHELL_PLUGIN_IDS: - _copy_source_to_artifact(checkouts[plugin_id], providers / plugin_id) - old_boot = f"e2-old-{os.getpid()}-{os.urandom(4).hex()}" - new_boot = f"e2-new-{os.getpid()}-{os.urandom(4).hex()}" - stage = sandbox / "runtime-python" - steam_source = checkouts["steam-mcp"] - child_code = textwrap.dedent(""" - import asyncio - import json - import shutil - import sys - from pathlib import Path - from agent.plugins.artifacts import ArtifactPointer, write_pointers - from agent.plugins.generation_activity_host import ActivityHost - from agent.plugins.generation_job_host import BackgroundJobActivityAdapter - from agent.plugins.manager import PluginManager - from agent.plugins.manifest import write_plugin_manifest - from agent.plugins.static_manifest import load_static_plugin_manifest - from bus.event_bus import EventBus - - async def main() -> None: - source = Path(sys.argv[1]) - providers = Path(sys.argv[2]) - cache = Path(sys.argv[3]) - workspace = Path(sys.argv[4]) - stage = Path(sys.argv[5]) - evidence = Path(sys.argv[6]) - - # Stage the complete stable/latest pair before Core discovery. - cache.mkdir(parents=True, exist_ok=True) - base = cache / "github" / "steam" - stable = base / ".artifacts" / "stable" - latest = base / ".artifacts" / "latest" - shutil.copytree(source, stable, ignore=shutil.ignore_patterns(".git", ".venv", "__pycache__")) - shutil.copytree(source, latest, ignore=shutil.ignore_patterns(".git", ".venv", "__pycache__")) - (stable / ".e2-stable-baseline").write_text("synthetic stable baseline", encoding="utf-8") - manifest = load_static_plugin_manifest(source) - for artifact in (stable, latest): - for runtime in manifest.python: - root = artifact / runtime.runtime_root - root.mkdir(parents=True, exist_ok=True) - (root / ".venv").symlink_to(stage, target_is_directory=True) - write_pointers( - base, - stable=ArtifactPointer(".artifacts/stable"), - latest=ArtifactPointer(".artifacts/latest"), - ) - write_plugin_manifest({"steam@github": True}, plugins_home=cache.parent) - data_root = workspace / "plugin-data" / "steam-github" - data_root.mkdir(parents=True, exist_ok=True) - (data_root / "steam_mcp_config.json").write_text( - json.dumps( - { - "steam_api_key": "test-only", - "steam_id": "76561198000000000", - "snapshot_interval_seconds": 3600, - } - ), - encoding="utf-8", - ) - - event_bus = EventBus() - manager = PluginManager( - [providers], - event_bus=event_bus, - workspace=workspace, - installed_cache_root=cache, - ) - manager.bind_activity_host( - ActivityHost( - ( BackgroundJobActivityAdapter( - manager.snapshot_store, - workspace=str(workspace), - ), - ) - ) - ) - await manager.load_all() - candidate = await manager.prepare_candidate("steam@github") - if candidate is None: - raise RuntimeError("child Core 未准备 Steam candidate") - publication = await manager.publish_prepared("steam@github") - if publication.get("publication_state") != "latest_ready": - raise RuntimeError(f"child Core candidate 未 latest_ready: {publication}") - evidence.write_text( - json.dumps( - { - "generation_id": candidate.generation_id, - "tx_id": candidate.reload_tx_id, - } - ), - encoding="utf-8", - ) - await asyncio.sleep(60) - - asyncio.run(main()) - """) - child_env = dict(os.environ) - child_env["AKASHIC_BOOT_ID"] = old_boot - child_env["AKASHIC_SUPERVISED"] = "1" - child_env["PYTHONPATH"] = str(ROOT) + os.pathsep + child_env.get("PYTHONPATH", "") - child_log = crash_root / "child.log" - process = subprocess.Popen( - ( - str(runtime_python), - "-c", - child_code, - str(steam_source), - str(providers), - str(cache_root), - str(workspace), - str(stage), - str(evidence_path), - ), - cwd=ROOT, - env=child_env, - stdout=child_log.open("w", encoding="utf-8"), - stderr=subprocess.STDOUT, - text=True, - ) - try: - process.wait(timeout=30) - except subprocess.TimeoutExpired: - process.send_signal(signal.SIGKILL) - _ = process.wait(timeout=5) - if process.returncode != -signal.SIGKILL: - log = child_log.read_text(encoding="utf-8", errors="replace") - raise RuntimeError( - f"Core crash child 未进入 SIGKILL probe 终点 {process.returncode}: {log[-2000:]}" - ) - if process.returncode != -9: - log = child_log.read_text(encoding="utf-8", errors="replace") - raise RuntimeError( - f"Core crash child 非预期退出 {process.returncode}: {log[-2000:]}" - ) - if not evidence_path.is_file(): - raise RuntimeError("Core crash child 未写入 candidate transaction evidence") - child_evidence = json.loads(evidence_path.read_text(encoding="utf-8")) - stale_before = _boot_process_ids(old_boot) - - # 2. A fresh supervised Core must normalize the exact pointer and journal. - previous_boot = os.environ.get("AKASHIC_BOOT_ID") - previous_supervised = os.environ.get("AKASHIC_SUPERVISED") - manager: PluginManager | None = None - recovery_error: str | None = None - pending: tuple[object, ...] = () - pointers = None - try: - os.environ["AKASHIC_BOOT_ID"] = new_boot - os.environ["AKASHIC_SUPERVISED"] = "1" - event_bus = EventBus() - manager = PluginManager( - [providers], - event_bus=event_bus, - workspace=workspace, - installed_cache_root=cache_root, - ) - manager.bind_activity_host( - ActivityHost( - ( - BackgroundJobActivityAdapter( - manager.snapshot_store, - workspace=str(workspace), - ), - ) - ) - ) - await manager.load_all() - pending = manager.reload_journal.pending_recovery() - pointers = read_pointers(cache_root / "github" / "steam") - except (OSError, RuntimeError, ValueError) as error: - recovery_error = str(error) or type(error).__name__ - finally: - if manager is not None: - await manager.terminate_all() - if previous_boot is None: - os.environ.pop("AKASHIC_BOOT_ID", None) - else: - os.environ["AKASHIC_BOOT_ID"] = previous_boot - if previous_supervised is None: - os.environ.pop("AKASHIC_SUPERVISED", None) - else: - os.environ["AKASHIC_SUPERVISED"] = previous_supervised - - # 3. Never leak the killed Core's child runtime; retain a blocked receipt if cleanup was manual. - stale_after_manager = _boot_process_ids(old_boot) - manual_cleanup = False - if stale_after_manager: - from agent.background.boot_guardian import _cleanup_boot_processes - - await asyncio.to_thread( - _cleanup_boot_processes, - boot_id=old_boot, - gateway_group_id=None, - ) - manual_cleanup = True - stale_after_cleanup = _boot_process_ids(old_boot) - pointer_ok = pointers is not None and pointers.stable == pointers.latest - recovery_ok = ( - recovery_error is None - and not pending - and pointer_ok - and not stale_after_manager - ) - status = "passed" if recovery_ok else "blocked" - return { - "id": "core-process-crash", - "status": status, - "old_boot_id": old_boot, - "new_boot_id": new_boot, - "child": child_evidence, - "stale_processes_before_restart": list(stale_before), - "stale_processes_after_manager": list(stale_after_manager), - "stale_processes_after_cleanup": list(stale_after_cleanup), - "manual_cleanup": manual_cleanup, - "pending_recovery_count": len(pending), - "pointer_normalized": pointer_ok, - "recovery_error": recovery_error, - "reason": ( - "Core startup did not clean the old boot owner automatically" - if stale_after_manager - else recovery_error - ), - } - - -async def _run_gate( - checkouts: Mapping[str, Path], - sandbox: Path, - bootstrap_python: Path, -) -> dict[str, object]: - """Run Shell, MCP candidate, and failure-cleanup checks in one Manager.""" - - # 1. Stage every installed stable/latest pointer before Manager discovery. - workspace = sandbox / "workspace" - cache_root = sandbox / "plugin-home" / "cache" - runtime_stage, runtime_python, runtime_requirements = _create_runtime_stage( - bootstrap_python, - sandbox, - checkouts, - ) - bases = _prepare_external_candidates(checkouts, cache_root, runtime_stage) - _write_formal_steam_config(workspace) - - # 2. Load Shell and the staged MCP stable artifacts through the real Manager oracle. - event_bus = EventBus() - manager = PluginManager( - plugin_dirs=[sandbox / "providers"], - event_bus=event_bus, - workspace=workspace, - installed_cache_root=cache_root, - ) - manager.bind_activity_host( - ActivityHost( - ( - BackgroundJobActivityAdapter( - manager.snapshot_store, - workspace=str(workspace), - ), - ) - ) - ) - root = None - shell_result: ( - tuple[tuple[str, ...], tuple[ScenarioEvidence, ...], list[dict[str, object]]] - | None - ) = None - runtime_evidence: list[RuntimeEvidence] = [] - in_process_failure: dict[str, object] | None = None - core_crash: dict[str, object] | None = None - try: - await manager.load_all() - shell_result = await _run_shell_scenarios(manager, sandbox) - for plugin_id in MCP_PLUGIN_IDS: - runtime_evidence.append( - await _probe_candidate(manager, plugin_id, bases, checkouts) - ) - in_process_failure = await _run_in_process_failure( - manager, bases, checkouts, runtime_stage - ) - core_crash = await _run_core_process_crash( - checkouts, - sandbox, - Path(sys.executable), - ) - root = ( - manager.current_snapshot.composition_root - if manager.current_snapshot is not None - else None - ) - finally: - await manager.terminate_all() - - if shell_result is None or root is None: - raise RuntimeError("E2 Gate 成功路径未保留稳定 Root 证据") - retained_failures = tuple( - f"{item.generation_id}:{item.error}" - for item in ( - manager.composition_generation_host.failure(item.generation_id) - for item in (evidence for evidence in runtime_evidence) - ) - if item is not None - ) - cleanup = CleanupEvidence( - shell_generation_ids=tuple( - generation.generation_id - for generation in ( - manager.generation(plugin_id) for plugin_id in SHELL_PLUGIN_IDS - ) - if generation is not None - ), - retained_runtime_failures=retained_failures, - cleanup_failures=tuple(str(item) for item in manager.cleanup_failures), - listeners=root.topology_view().listeners, - effects=root.receipt().effects, - ) - if cleanup.retained_runtime_failures or cleanup.cleanup_failures or cleanup.effects: - raise RuntimeError(f"E2 cleanup evidence 未清零: {cleanup}") - return { - "runtime_requirements": list(runtime_requirements), - "shell": { - "listeners": list(shell_result[0]), - "scenarios": [asdict(item) for item in shell_result[1]], - "invocations": shell_result[2], - }, - "runtime": [asdict(item) for item in runtime_evidence], - "in_process_failure": in_process_failure, - "core_process_crash": core_crash, - "cleanup": asdict(cleanup), - } - - -def _write_report(path: Path, report: Mapping[str, object]) -> None: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="运行集中 v3 插件 E2 Gate") - parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) - parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) - parser.add_argument("--runtime-python", type=Path, default=Path(sys.executable)) - parser.add_argument("--require-clean-core", action="store_true") - return parser.parse_args() - - -def main() -> int: - """Run the Gate and return zero only after every required oracle passes.""" - - args = _parse_args() - report_path = args.report.resolve() - checked_at = datetime.now(UTC).isoformat() - core_status = _git_output(ROOT, "status", "--porcelain").splitlines() - base_report: dict[str, object] = { - "status": "blocked", - "gate_version": GATE_VERSION, - "checked_at": checked_at, - "core": { - "head": _git_output(ROOT, "rev-parse", "HEAD"), - "tree": _git_output(ROOT, "rev-parse", "HEAD^{tree}"), - "dirty_status": core_status, - }, - "lock": str(args.lock.resolve()), - "lock_sha256": None, - "scenario_profile": SCENARIO_PROFILE, - "scenario_catalog_sha256": _scenario_catalog_sha256(), - "scenario_catalog": [asdict(item) for item in SCENARIO_CATALOG], - "plugins": [], - "cases": [], - "blockers": [], - "failures": [], - } - try: - if args.require_clean_core and core_status: - raise GateBlocked(f"核心工作树不干净: {core_status}") - lock_path = args.lock.resolve() - base_report["lock_sha256"] = _sha256(lock_path) - locks = _load_lock(lock_path) - base_report["lock_plugins"] = [asdict(item) for item in locks] - bootstrap_python = _runtime_interpreter(args.runtime_python) - with tempfile.TemporaryDirectory(prefix="akashic-plugin-v3-e2-") as raw: - sandbox = Path(raw) - providers = sandbox / "providers" - providers.mkdir() - checkouts: dict[str, Path] = {} - evidences: list[PluginEvidence] = [] - for lock in locks: - checkout = ( - providers / lock.id - if lock.id in SHELL_PLUGIN_IDS - else sandbox / "sources" / lock.id - ) - checkouts[lock.id] = checkout - evidences.append(_checkout_locked_plugin(lock, checkout)) - base_report["plugins"] = [asdict(item) for item in evidences] - base_report["runtime_python"] = str(bootstrap_python) - gate_result = asyncio.run(_run_gate(checkouts, sandbox, bootstrap_python)) - base_report.update(gate_result) - core_case = cast(dict[str, object], gate_result.get("core_process_crash", {})) - if core_case.get("status") == "blocked": - base_report["status"] = "blocked" - base_report["blockers"] = [ - str(core_case.get("reason") or "Core process crash recovery blocked") - ] - print( - f"plugin v3 concentrated E2 gate blocked: {report_path}", - file=sys.stderr, - ) - status = 2 - elif core_case.get("status") == "failed": - base_report["status"] = "failed" - base_report["failures"] = [ - str(core_case.get("reason") or "Core process crash recovery failed") - ] - print( - f"plugin v3 concentrated E2 gate failed: {report_path}", - file=sys.stderr, - ) - status = 1 - else: - base_report["status"] = "passed" - print(f"plugin v3 concentrated E2 gate passed: {report_path}") - status = 0 - except GateBlocked as error: - message = str(error) or type(error).__name__ - base_report["blockers"] = [message] - print(f"plugin v3 concentrated E2 gate blocked: {message}", file=sys.stderr) - status = 2 - except ( - OSError, - RuntimeError, - ValueError, - json.JSONDecodeError, - subprocess.CalledProcessError, - ) as error: - message = str(error) or type(error).__name__ - base_report["failures"] = [message] - print(f"plugin v3 concentrated E2 gate failed: {message}", file=sys.stderr) - status = 1 - finally: - _write_report(report_path, base_report) - return status - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/docker/debug/plugin_v3_e4_gate.py b/docker/debug/plugin_v3_e4_gate.py deleted file mode 100644 index 7f5f1bf42..000000000 --- a/docker/debug/plugin_v3_e4_gate.py +++ /dev/null @@ -1,746 +0,0 @@ -#!/usr/bin/env python3 -"""Run the final pure-v3 rehearsal against a disposable workspace copy.""" - -from __future__ import annotations - -import argparse -import asyncio -import hashlib -import json -import os -import shutil -import sqlite3 -import subprocess -import sys -import tempfile -from collections.abc import Mapping -from datetime import UTC, datetime -from pathlib import Path -from typing import Any, cast -from urllib.parse import quote - -ROOT = Path(__file__).resolve().parents[2] -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from docker.debug import plugin_v3_e1_gate as e1_gate # noqa: E402 -from scripts.container_rehearsal.prepare import prepare_rehearsal # noqa: E402 -from scripts.container_rehearsal.policy import excluded_reason # noqa: E402 - -DEFAULT_LOCK = ROOT / "docker/debug/plugin-v3-fleet.lock.json" -DEFAULT_REPORT = ROOT / "docker/debug/reports/plugin-v3-e4" / "gate.json" -DEFAULT_E1_REPORT = ROOT / "docker/debug/reports/plugin-v3-e1" / "gate.json" -DEFAULT_E2_REPORT = ROOT / "docker/debug/reports/plugin-v3-e2" / "gate.json" -DEFAULT_E3_REPORT = ROOT / "docker/debug/reports/plugin-v3-e3" / "gate.json" -DEFAULT_PASSIVE_REPORT = ( - ROOT / "docker/debug/reports/plugin-passive-webui-v3" / "gate.json" -) - -GATE_VERSION = 1 -SCENARIO_PROFILE = "plugin-v3-e4-copied-workspace-rehearsal-v1" -SQLITE_HEADER = b"SQLite format 3\x00" -E2_PROFILE = "plugin-v3-e2-shell-v1" -E3_PROFILE = "plugin-v3-e3-fleet-channel-proactive-v3" -PASSIVE_PROFILE = "citation-meme-webui-v3-v1" -E1_SCENARIOS = ("runtime_boot_akasha",) -E1_RUNTIME_ENGINES = ("akasha",) -E1_PLUGINS = { - "akasha", - "citation", - "meme", - "emotion", - "observe", - "proactive_feedback", - "plugin_undo", -} -E2_PLUGINS = { - "shell_restore", - "shell_safety", - "calendar-mcp", - "feed-mcp", - "fitbit-mcp", - "steam-mcp", -} -E3_PLUGINS = { - "setup_helper", - "status_commands", - "emotion", - "calendar-mcp", - "feed-mcp", - "fitbit-mcp", - "steam-mcp", - "huayue-skills", - "github_watch", - "feishu", - "qqbot", - "citation", - "meme", -} - - -class GateBlocked(RuntimeError): - """Missing prerequisite evidence or unavailable provider input.""" - - -class GateFailure(RuntimeError): - """Copied-workspace invariant violation.""" - - -def _resolve_tmp_root(value: Path | None) -> Path | None: - """解析调用方可选的临时目录。""" - - if value is None: - return None - root = value.expanduser().resolve() - if not root.is_dir(): - raise GateFailure(f"E4 tmp root 不是已存在目录: {root}") - return root - - -def _sha256_file(path: Path) -> str: - """Hash a file without loading it all into memory.""" - - digest = hashlib.sha256() - with path.open("rb") as stream: - while chunk := stream.read(1024 * 1024): - digest.update(chunk) - return digest.hexdigest() - - -def _freeze(value: object) -> object: - """Convert SQLite values to stable, non-secret evidence values.""" - - if isinstance(value, bytes): - return {"sha256": hashlib.sha256(value).hexdigest(), "length": len(value)} - if value is None or isinstance(value, str | int | float | bool): - return value - return repr(value) - - -def _digest_records(records: list[dict[str, object]]) -> str: - encoded = json.dumps( - records, ensure_ascii=False, sort_keys=True, separators=(",", ":") - ).encode("utf-8") - return hashlib.sha256(encoded).hexdigest() - - -def _is_sqlite_runtime_sidecar(path: Path) -> bool: - """只在相邻主文件确为 SQLite 时识别它的运行 sidecar。""" - - # 1. 后缀条目必须是实体普通文件。 - if path.is_symlink() or not path.is_file(): - return False - suffix = next( - ( - candidate - for candidate in ("-wal", "-shm", "-journal") - if path.name.endswith(candidate) - ), - None, - ) - if suffix is None: - return False - - # 2. 同名主文件必须携带 SQLite 文件头,避免排除插件自有普通文件。 - database = path.with_name(path.name[: -len(suffix)]) - if database.is_symlink() or not database.is_file(): - return False - with database.open("rb") as stream: - return stream.read(len(SQLITE_HEADER)) == SQLITE_HEADER - - -def _tree_summary( - root: Path, - *, - include_entries: bool = False, - exclude_workspace_runtime: bool = False, - exclude_sqlite_sidecars: bool = False, -) -> dict[str, object]: - """Summarize a tree without following symlinks or exposing file contents.""" - - records: list[dict[str, object]] = [] - if not root.exists(): - missing: list[dict[str, object]] = [{"kind": "missing", "path": "."}] - return { - "path": str(root), - "exists": False, - "digest": _digest_records(missing), - "file_count": 0, - "directory_count": 0, - "symlink_count": 0, - **({"entries": missing} if include_entries else {}), - } - - def visit(directory: Path) -> None: - for child in sorted(directory.iterdir(), key=lambda item: item.name): - relative = child.relative_to(root).as_posix() - if exclude_sqlite_sidecars and _is_sqlite_runtime_sidecar(child): - continue - if ( - exclude_workspace_runtime - and excluded_reason(Path(relative), is_symlink=child.is_symlink()) - is not None - ): - continue - if child.is_symlink(): - records.append( - {"kind": "symlink", "path": relative, "target": os.readlink(child)} - ) - elif child.is_dir(): - records.append({"kind": "directory", "path": relative}) - visit(child) - elif child.is_file(): - records.append( - { - "kind": "file", - "path": relative, - "size": child.stat().st_size, - "sha256": _sha256_file(child), - } - ) - else: - raise GateFailure(f"不支持的 Workspace 文件系统条目: {child}") - - if root.is_symlink(): - raise GateFailure(f"Workspace root 不能是符号链接: {root}") - if root.is_file(): - records.append( - { - "kind": "file", - "path": ".", - "size": root.stat().st_size, - "sha256": _sha256_file(root), - } - ) - elif root.is_dir(): - visit(root) - else: - raise GateFailure(f"Workspace root 不是实体文件或目录: {root}") - records.sort(key=lambda item: str(item["path"])) - return { - "path": str(root), - "exists": True, - "digest": _digest_records(records), - "file_count": sum(item["kind"] == "file" for item in records), - "directory_count": sum(item["kind"] == "directory" for item in records), - "symlink_count": sum(item["kind"] == "symlink" for item in records), - **({"entries": records} if include_entries else {}), - } - - -def _artifact_inventory( - root: Path, *, exclude_sqlite_sidecars: bool = False -) -> dict[str, object]: - """Inventory artifact and pointer files by digest, never by contents.""" - - summary = _tree_summary( - root, - include_entries=True, - exclude_sqlite_sidecars=exclude_sqlite_sidecars, - ) - entries = cast(list[dict[str, object]], summary.get("entries", [])) - artifacts: list[dict[str, object]] = [] - pointers: list[dict[str, object]] = [] - for entry in entries: - if entry.get("kind") != "file": - continue - relative = str(entry["path"]) - path = Path(relative) - if ".artifacts" in path.parts: - artifacts.append(entry) - if ( - path.name in {"stable.json", "latest.json", "pointers.json"} - or "pointer" in path.name.lower() - ): - pointers.append(entry) - return { - "root": str(root), - "tree_digest": summary["digest"], - "artifact_files": artifacts, - "pointer_files": pointers, - "artifact_digest": _digest_records(artifacts), - "pointer_digest": _digest_records(pointers), - } - - -def _sqlite_snapshot(path: Path) -> dict[str, object]: - """Read-only check SQLite integrity and canonical existing message rows.""" - - if not path.is_file(): - raise GateBlocked(f"sessions.db 不存在: {path}") - uri = f"file:{quote(str(path.resolve()))}?mode=ro" - try: - connection = sqlite3.connect(uri, uri=True) - except sqlite3.Error as error: - raise GateFailure(f"无法以只读方式打开 sessions.db: {error}") from error - try: - integrity = str(connection.execute("PRAGMA integrity_check").fetchone()[0]) - if integrity != "ok": - raise GateFailure(f"sessions.db integrity_check 失败: {integrity}") - required = { - "id", - "session_key", - "seq", - "role", - "content", - "tool_chain", - "extra", - "ts", - } - columns = { - str(row[1]) for row in connection.execute("PRAGMA table_info(messages)") - } - if not required.issubset(columns): - raise GateBlocked( - "sessions.db 缺少 append-only messages schema: " - + ",".join(sorted(required - columns)) - ) - rows = { - str(row[0]): json.dumps( - [_freeze(item) for item in row[1:]], ensure_ascii=False, sort_keys=True - ) - for row in connection.execute( - "SELECT id, session_key, seq, role, content, tool_chain, extra, ts FROM messages ORDER BY id" - ) - } - return { - "path": str(path), - "integrity": integrity, - "message_count": len(rows), - "message_rows_digest": _digest_records( - [{"id": key, "row": rows[key]} for key in sorted(rows)] - ), - "_message_rows": rows, - } - finally: - connection.close() - - -def _append_only_evidence( - before: Mapping[str, object], after: Mapping[str, object], *, label: str -) -> dict[str, object]: - """Prove existing canonical messages were neither deleted nor rewritten.""" - - old = cast(dict[str, str], before["_message_rows"]) - new = cast(dict[str, str], after["_message_rows"]) - removed = sorted(set(old) - set(new)) - changed = sorted(key for key in set(old) & set(new) if old[key] != new[key]) - if removed or changed: - raise GateFailure( - f"{label} 违反 append-only: removed={removed[:5]} changed={changed[:5]}" - ) - return { - "status": "passed", - "label": label, - "integrity_before": before["integrity"], - "integrity_after": after["integrity"], - "existing_message_count": len(old), - "new_message_count": len(new) - len(old), - "removed": [], - "changed": [], - } - - -def _read_json(path: Path, label: str) -> dict[str, object]: - if not path.is_file(): - raise GateBlocked(f"{label} report 不存在: {path}") - try: - payload = json.loads(path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise GateBlocked( - f"{label} report 无法读取: {type(error).__name__}: {error}" - ) from error - if not isinstance(payload, dict): - raise GateBlocked(f"{label} report 顶层必须是 object") - return cast(dict[str, object], payload) - - -def _mapping(value: object, label: str) -> dict[str, object]: - if not isinstance(value, dict): - raise GateBlocked(f"{label} 必须是 object") - return cast(dict[str, object], value) - - -def _report_lock_sha(report: Mapping[str, object], label: str) -> str: - direct = report.get("lock_sha256") - if isinstance(direct, str): - return direct - lock = report.get("lock") - if isinstance(lock, dict): - nested = cast(dict[str, object], lock).get("sha256") - if isinstance(nested, str): - return nested - raise GateBlocked(f"{label} report 缺少 lock_sha256") - - -def _validate_core_identity( - report: Mapping[str, object], label: str, current: Mapping[str, str], lock_sha: str -) -> dict[str, object]: - core = _mapping(report.get("core"), f"{label}.core") - head, tree = core.get("head"), core.get("tree") - if head != current["head"] or tree != current["tree"]: - raise GateBlocked( - f"{label} core identity 不是当前 HEAD/tree: report={head}/{tree} " - f"current={current['head']}/{current['tree']}" - ) - actual_lock_sha = _report_lock_sha(report, label) - if actual_lock_sha != lock_sha: - raise GateBlocked( - f"{label} lock identity 不匹配: report={actual_lock_sha} current={lock_sha}" - ) - return {"head": head, "tree": tree, "lock_sha256": actual_lock_sha} - - -def _mapping_by_id(value: object, label: str) -> dict[str, dict[str, object]]: - if not isinstance(value, list): - raise GateBlocked(f"{label} 必须是列表") - result: dict[str, dict[str, object]] = {} - for raw in cast(list[object], value): - item = _mapping(raw, f"{label} item") - scenario_id = item.get("id") - if isinstance(scenario_id, str): - result[scenario_id] = item - return result - - -def _validate_report( - path: Path, label: str, current: Mapping[str, str], lock_sha: str -) -> dict[str, object]: - """Validate one final report and return compact identity evidence.""" - - report = _read_json(path, label) - if report.get("status") != "passed": - raise GateBlocked( - f"{label} report.status 不是 passed: {report.get('status')!r}" - ) - identity = _validate_core_identity(report, label, current, lock_sha) - if label == "E1": - if report.get("phase") != "e1": - raise GateBlocked(f"E1 phase 不匹配: {report.get('phase')!r}") - scenarios = _mapping_by_id(report.get("scenarios"), "E1.scenarios") - for scenario_id in E1_SCENARIOS: - if scenarios.get(scenario_id, {}).get("status") != "passed": - raise GateBlocked(f"E1 缺少 passed 场景: {scenario_id}") - runtime = _mapping(report.get("runtime"), "E1.runtime") - for engine in E1_RUNTIME_ENGINES: - if engine not in runtime: - raise GateBlocked(f"E1 缺少 {engine} data-read boot evidence") - elif label == "E2": - if report.get("scenario_profile") != E2_PROFILE: - raise GateBlocked( - f"E2 scenario_profile 不匹配: {report.get('scenario_profile')!r}" - ) - crash = _mapping(report.get("core_process_crash"), "E2.core_process_crash") - if crash.get("status") not in {"passed", None}: - raise GateBlocked(f"E2 Core crash recovery 未通过: {crash}") - elif label == "E3": - if report.get("scenario_profile") != E3_PROFILE: - raise GateBlocked( - f"E3 scenario_profile 不匹配: {report.get('scenario_profile')!r}" - ) - runtime = _mapping(report.get("runtime"), "E3.runtime") - for field in ("channel", "message_push", "channel_cleanup"): - if field not in runtime: - raise GateBlocked(f"E3 runtime 缺少 {field}") - elif label == "Passive WebUI": - if report.get("scenario_profile") != PASSIVE_PROFILE: - raise GateBlocked( - f"Passive WebUI scenario_profile 不匹配: {report.get('scenario_profile')!r}" - ) - runtime = _mapping(report.get("runtime"), "Passive WebUI.runtime") - if runtime.get("status") != "passed": - raise GateBlocked("Passive WebUI runtime.status 不是 passed") - cleanup = _mapping(report.get("cleanup"), "Passive WebUI.cleanup") - if cleanup.get("residuals") != [] or cleanup.get("sandbox_removed") is not True: - raise GateBlocked("Passive WebUI cleanup 证据不完整") - else: - raise GateFailure(f"未知 report label: {label}") - return { - "label": label, - "path": str(path), - "status": "passed", - "scenario_profile": report.get("scenario_profile", "e1"), - "core": identity, - } - - -def validate_final_reports( - *, - e1_report: Path, - e2_report: Path, - e3_report: Path, - passive_webui_report: Path, - current_identity: Mapping[str, str], - lock_sha256: str, -) -> dict[str, object]: - """Consume exact final reports without rerunning any upstream E gate.""" - - reports = [ - _validate_report(e1_report, "E1", current_identity, lock_sha256), - _validate_report(e2_report, "E2", current_identity, lock_sha256), - _validate_report(e3_report, "E3", current_identity, lock_sha256), - _validate_report( - passive_webui_report, "Passive WebUI", current_identity, lock_sha256 - ), - ] - return {"status": "passed", "reports": reports} - - -def _git_output(*args: str) -> str: - completed = subprocess.run( - ["git", *args], - cwd=ROOT, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=False, - ) - if completed.returncode != 0: - raise GateFailure(f"git {' '.join(args)} 失败: {completed.stderr.strip()}") - return completed.stdout.strip() - - -def _current_identity(lock_path: Path) -> dict[str, str]: - if not lock_path.is_file(): - raise GateBlocked(f"fleet lock 不存在: {lock_path}") - return { - "head": _git_output("rev-parse", "HEAD"), - "tree": _git_output("rev-parse", "HEAD^{tree}"), - "lock_sha256": _sha256_file(lock_path), - } - - -def _fleet_coverage(lock_path: Path) -> dict[str, object]: - """Report exact fleet IDs not represented by completed E1-E3 lanes.""" - - try: - payload = json.loads(lock_path.read_text(encoding="utf-8")) - except (OSError, UnicodeError, json.JSONDecodeError) as error: - raise GateBlocked( - f"fleet lock 无法读取: {type(error).__name__}: {error}" - ) from error - if not isinstance(payload, dict) or not isinstance(payload.get("plugins"), list): - raise GateBlocked("fleet lock.plugins 必须是列表") - expected: set[str] = set() - for raw in cast(list[object], payload["plugins"]): - plugin = _mapping(raw, "fleet lock plugin") - plugin_id = plugin.get("id") - if not isinstance(plugin_id, str) or not plugin_id: - raise GateBlocked(f"fleet lock plugin.id 无效: {plugin_id!r}") - expected.add(plugin_id) - covered = E1_PLUGINS | E2_PLUGINS | E3_PLUGINS - missing = sorted(expected - covered) - return { - "status": "passed" if not missing else "blocked", - "expected_ids": sorted(expected), - "covered_ids": sorted(expected & covered), - "missing_ids": missing, - "reason": ( - "full fleet exact provider/runtime coverage unavailable" - if missing - else None - ), - } - - -def _copy_tree(source: Path, target: Path) -> None: - """Copy a prepared workspace into another disposable scenario root.""" - - shutil.copytree(source, target, symlinks=True) - - -async def _run_builtin_boot(workspace: Path) -> dict[str, object]: - """Boot real in-tree Akasha from copied data.""" - - plugin_dirs = e1_gate._plugin_dirs({}) # pyright: ignore[reportPrivateUsage] - evidence: dict[str, object] = {} - for engine in E1_RUNTIME_ENGINES: - runtime_workspace = workspace.parent / f"e4-runtime-{engine}" - _copy_tree(workspace, runtime_workspace) - bundle: Any | None = None - try: - bundle = await e1_gate._open_runtime( # pyright: ignore[reportPrivateUsage] - runtime_workspace, plugin_dirs - ) - boot = await e1_gate._probe_boot( - bundle - ) # pyright: ignore[reportPrivateUsage] - mobile = _mapping(boot.get("mobile_query"), "Akasha mobile query") - if mobile.get("status") != "passed": - raise GateBlocked(f"Akasha copied data-read boot unavailable: {mobile}") - evidence[engine] = {"status": "passed", "boot": boot} - finally: - if bundle is not None: - cleanup = await e1_gate._close_runtime( - bundle - ) # pyright: ignore[reportPrivateUsage] - if cleanup: - raise GateFailure(f"{engine} graceful stop cleanup 失败: {cleanup}") - return {"status": "passed", "engines": evidence} - - -def _sanitize_sqlite(snapshot: Mapping[str, object]) -> dict[str, object]: - return {key: value for key, value in snapshot.items() if key != "_message_rows"} - - -def _parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="运行 pure-v3 copied-workspace E4 rehearsal" - ) - parser.add_argument("--source-workspace", type=Path, required=True) - parser.add_argument("--source-config", type=Path, required=True) - parser.add_argument("--plugin-home", type=Path, required=True) - parser.add_argument("--lock", type=Path, default=DEFAULT_LOCK) - parser.add_argument("--e1-report", type=Path, default=DEFAULT_E1_REPORT) - parser.add_argument("--e2-report", type=Path, default=DEFAULT_E2_REPORT) - parser.add_argument("--e3-report", type=Path, default=DEFAULT_E3_REPORT) - parser.add_argument( - "--passive-webui-report", type=Path, default=DEFAULT_PASSIVE_REPORT - ) - parser.add_argument("--report", type=Path, default=DEFAULT_REPORT) - parser.add_argument("--tmp-root", type=Path) - return parser.parse_args() - - -async def _run_runtime(args: argparse.Namespace, report: dict[str, object]) -> None: - lock_path = args.lock.resolve() - current = _current_identity(lock_path) - report["core"] = current - report["reports"] = validate_final_reports( - e1_report=args.e1_report.resolve(), - e2_report=args.e2_report.resolve(), - e3_report=args.e3_report.resolve(), - passive_webui_report=args.passive_webui_report.resolve(), - current_identity=current, - lock_sha256=current["lock_sha256"], - ) - report["fleet_coverage"] = _fleet_coverage(lock_path) - source_workspace = args.source_workspace.resolve(strict=True) - source_config = args.source_config.resolve(strict=True) - plugin_home = args.plugin_home.resolve(strict=True) - source_before = { - "workspace": _tree_summary(source_workspace, exclude_workspace_runtime=True), - "config": _tree_summary(source_config), - "plugin_home": _tree_summary(plugin_home), - "plugin_data": _artifact_inventory( - source_workspace / "plugin-data", exclude_sqlite_sidecars=True - ), - "artifact_pointer": _artifact_inventory(plugin_home), - } - source_db_before = _sqlite_snapshot(source_workspace / "sessions.db") - report["source_before"] = { - **source_before, - "sessions_db": _sanitize_sqlite(source_db_before), - } - tmp_parent = _resolve_tmp_root(args.tmp_root) - with tempfile.TemporaryDirectory( - prefix="akashic-plugin-v3-e4-", dir=tmp_parent - ) as raw: - target = Path(raw) / "rehearsal" - manifest = prepare_rehearsal( - source_workspace=source_workspace, - source_config=source_config, - plugin_home=plugin_home, - target=target, - ) - copied_workspace = target / "workspace" - copied_db_before = _sqlite_snapshot(copied_workspace / "sessions.db") - before_artifact = _artifact_inventory(target / "plugin-home") - report["rehearsal_copy"] = { - "status": "passed", - "manifest": str(manifest), - "target": str(target), - "workspace": _tree_summary(copied_workspace), - "plugin_data": _artifact_inventory( - copied_workspace / "plugin-data", exclude_sqlite_sidecars=True - ), - "artifact_pointer": before_artifact, - } - report["builtin_e1_data_read_boot"] = await _run_builtin_boot(copied_workspace) - copied_db_after = _sqlite_snapshot(copied_workspace / "sessions.db") - report["copied_sessions_append_only"] = _append_only_evidence( - copied_db_before, copied_db_after, label="rehearsal copy sessions.db" - ) - copied_artifact_after = _artifact_inventory(target / "plugin-home") - if before_artifact != copied_artifact_after: - raise GateFailure("copied artifact/pointer inventory 在生命周期中发生变化") - report["artifact_pointer_after"] = copied_artifact_after - report["plugin_data_after"] = _artifact_inventory( - copied_workspace / "plugin-data", exclude_sqlite_sidecars=True - ) - report["graceful_stop_cleanup"] = { - "status": "passed", - "runtime_directories_removed_with_rehearsal": True, - } - source_after = { - "workspace": _tree_summary(source_workspace, exclude_workspace_runtime=True), - "config": _tree_summary(source_config), - "plugin_home": _tree_summary(plugin_home), - "plugin_data": _artifact_inventory( - source_workspace / "plugin-data", exclude_sqlite_sidecars=True - ), - "artifact_pointer": _artifact_inventory(plugin_home), - } - source_db_after = _sqlite_snapshot(source_workspace / "sessions.db") - report["source_after"] = { - **source_after, - "sessions_db": _sanitize_sqlite(source_db_after), - } - report["source_sessions_append_only"] = _append_only_evidence( - source_db_before, source_db_after, label="source sessions.db" - ) - if source_before != source_after: - raise GateFailure("source workspace/config/plugin-home 生命周期摘要发生变化") - report["source_unchanged"] = True - - -def main() -> int: - """Run E4 and return blocked/failed status without hiding evidence.""" - - args = _parse_args() - report_path = args.report.resolve() - report: dict[str, object] = { - "status": "failed", - "gate_version": GATE_VERSION, - "scenario_profile": SCENARIO_PROFILE, - "checked_at": datetime.now(UTC).isoformat(), - "blockers": [], - "failures": [], - } - exit_code = 1 - try: - asyncio.run(_run_runtime(args, report)) - coverage = cast(dict[str, object], report.get("fleet_coverage", {})) - if coverage.get("status") != "passed": - report["status"] = "blocked" - report["blockers"] = [str(coverage.get("reason") or coverage)] - exit_code = 2 - else: - report["status"] = "passed" - exit_code = 0 - except GateBlocked as error: - report["status"] = "blocked" - report["blockers"] = [f"{type(error).__name__}: {error}"] - exit_code = 2 - except ( - GateFailure, - OSError, - RuntimeError, - sqlite3.Error, - subprocess.SubprocessError, - ) as error: - report["status"] = "failed" - report["failures"] = [f"{type(error).__name__}: {error}"] - exit_code = 1 - finally: - report_path.parent.mkdir(parents=True, exist_ok=True) - report_path.write_text( - json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - print(f"plugin v3 E4 {report['status']}: {report_path}") - return exit_code - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/docker/debug/plugin_v3_fleet_gate.py b/docker/debug/plugin_v3_fleet_gate.py index 9fe939424..d14f0949a 100644 --- a/docker/debug/plugin_v3_fleet_gate.py +++ b/docker/debug/plugin_v3_fleet_gate.py @@ -3,6 +3,7 @@ import argparse import ast import hashlib +import importlib import json import re import subprocess @@ -16,6 +17,8 @@ from typing import cast ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) DEFAULT_LOCK = ROOT / "docker" / "debug" / "plugin-v3-fleet.lock.json" DEFAULT_REPORT = ROOT / "docker" / "debug" / "reports" / "plugin-v3-fleet" / "gate.json" GATE_VERSION = 1 @@ -98,9 +101,6 @@ "mobile_ui_query", } ) -E2E_NOT_RUN_REASON = ( - "static Gate 第一阶段不执行 runtime E2E;需最终 Core/plugin 组合与受控环境" -) GIT_COMMAND_TIMEOUT_SECONDS = 30 @@ -113,89 +113,6 @@ class PluginLock: change_source_pr_head: str -@dataclass(frozen=True, slots=True) -class E2ECase: - id: str - title: str - required_plugins: tuple[str, ...] - oracle: tuple[str, ...] - - -E2E_CATALOG = ( - E2ECase( - "E1", - "Passive/Data/Mobile", - ( - "akasha", - "citation", - "meme", - "emotion", - "observe", - "proactive_feedback", - "plugin_undo", - ), - ( - "prompt/recall/metadata/media", - "bounded mobile query and lease", - "append-only SessionDB write set", - ), - ), - E2ECase( - "E2", - "Tool/MCP/Process", - ( - "shell_restore", - "shell_safety", - "calendar-mcp", - "feed-mcp", - "fitbit-mcp", - "steam-mcp", - ), - ( - "transform/authorize/invoke", - "MCP and process readiness", - "cancel and process cleanup", - "controlled external read-only calls", - ), - ), - E2ECase( - "E3", - "Fleet/Channel/Proactive", - ( - "setup_helper", - "status_commands", - "feishu", - "qqbot", - "emotion", - "calendar-mcp", - "feed-mcp", - "fitbit-mcp", - "steam-mcp", - "huayue-skills", - "github_watch", - ), - ( - "full boot and catalog", - "candidate discard and promotion", - "loopback channel recording", - "fixed-clock background-job restart", - "controlled repository probe", - ), - ), - E2ECase( - "E4", - "Production Rehearsal", - ("E1", "E2", "E3"), - ( - "copied-workspace database integrity", - "complete write set", - "artifact/pointer and restart", - "stop cleanup and restore evidence", - ), - ), -) - - class GateError(RuntimeError): """A reproducible static Gate input or evidence failure.""" @@ -422,7 +339,7 @@ def _matching_refs(output: str, sha: str) -> tuple[str, ...]: def _inspect_static_plugin(root: Path, plugin_id: str) -> dict[str, object]: - """Inspect manifest, v3 namespace, and generic v2 imports without importing code.""" + """Inspect manifest, v3 namespace, and declared Core imports.""" # 1. Parse the import-free manifest and choose its declared entrypoint. manifest, manifest_errors = _inspect_manifest(root) @@ -435,6 +352,7 @@ def _inspect_static_plugin(root: Path, plugin_id: str) -> dict[str, object]: # 3. Scan production Python sources for generic v2 import and class edges. forbidden = _find_forbidden_v2_imports(root) forbidden_classes = _find_forbidden_v2_classes(root) + missing_core_imports = _find_missing_core_imports(root) errors = [*manifest_errors, *cast(list[str], namespace["errors"])] manifest_name = manifest.get("name") namespace_name = namespace.get("name") @@ -452,6 +370,8 @@ def _inspect_static_plugin(root: Path, plugin_id: str) -> dict[str, object]: errors.append("发现 generic v2 import") if forbidden_classes: errors.append("发现 legacy v2 Plugin class/fixed methods") + if missing_core_imports: + errors.append("发现当前 Core 不再导出的导入符号") return { "status": "passed" if not errors else "failed", "plugin_id": plugin_id, @@ -459,10 +379,65 @@ def _inspect_static_plugin(root: Path, plugin_id: str) -> dict[str, object]: "namespace": namespace, "forbidden_v2_imports": forbidden, "forbidden_v2_classes": forbidden_classes, + "missing_core_imports": missing_core_imports, "errors": errors, } +def _find_missing_core_imports(root: Path) -> list[dict[str, object]]: + """Reject plugin imports that the current Core cannot satisfy.""" + + violations: list[dict[str, object]] = [] + modules: dict[str, object] = {} + for source in sorted(root.rglob("*.py")): + if any( + part in {".git", ".venv", "__pycache__", "scripts", "tests"} + for part in source.parts + ): + continue + try: + tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source)) + except (OSError, SyntaxError): + continue + for node in ast.walk(tree): + if ( + not isinstance(node, ast.ImportFrom) + or node.level + or node.module is None + or not node.module.startswith("agent.") + ): + continue + try: + if node.module not in modules: + modules[node.module] = importlib.import_module(node.module) + module = modules[node.module] + except (ImportError, ModuleNotFoundError) as error: + violations.append( + { + "path": _relative_or_name(source, root), + "line": node.lineno, + "module": node.module, + "error": f"{type(error).__name__}: {error}", + } + ) + continue + missing = sorted( + alias.name + for alias in node.names + if alias.name != "*" and not hasattr(module, alias.name) + ) + if missing: + violations.append( + { + "path": _relative_or_name(source, root), + "line": node.lineno, + "module": node.module, + "names": missing, + } + ) + return violations + + def _inspect_manifest(root: Path) -> tuple[dict[str, object], list[str]]: manifest_path = root / STATIC_MANIFEST_FILENAME evidence: dict[str, object] = { @@ -721,9 +696,7 @@ def _build_report( plugins: tuple[dict[str, object], ...] | list[dict[str, object]], errors: list[str], ) -> dict[str, object]: - """Build one report whose runtime E2E entries cannot claim execution.""" - - e2e = _e2e_report() + """Build one report for the locked fleet's static contract.""" return { "status": "passed" if not errors else "failed", "phase": "static", @@ -740,31 +713,10 @@ def _build_report( "status": "passed" if not errors else "failed", "error_count": len(errors), }, - "e2e": e2e, "errors": list(errors), } -def _e2e_report() -> dict[str, object]: - catalog = [ - { - **asdict(case), - "required_plugins": list(case.required_plugins), - "oracle": list(case.oracle), - "status": "not_run", - "executed": False, - "reason": E2E_NOT_RUN_REASON, - } - for case in E2E_CATALOG - ] - return { - "status": "not_run", - "catalog_sha256": _json_sha256(catalog), - "catalog": catalog, - "reason": E2E_NOT_RUN_REASON, - } - - def _core_evidence() -> dict[str, object]: dirty_status = tuple(_git_output(ROOT, "status", "--porcelain").splitlines()) commit = _git_output(ROOT, "rev-parse", "HEAD") diff --git a/docs/INDEX.md b/docs/INDEX.md index e5385a901..51accaf4d 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -111,21 +111,21 @@ | 正式启动、Supervisor、自重启、停止信号 | `projectneed` RUN-001~RUN-004 → [Linux Supervisor 安全自重启提议](design/linux-supervisor-safe-self-restart.md) → [`docker/debug/README.md`](../docker/debug/README.md) | `main.py`、`agent/supervisor.py`、`agent/restart.py`、`agent/tools/agent_restart.py`、`scripts/stop-runtime.sh`、restart Gate 报告 | | 容器、云主机运行适配、Host Bridge、插件 Workload、hua-home迁移 | `projectneed` RUN-013~RUN-016、PLG-017、WSP-005~WSP-006 → [0032](decisions/0032-host-bridge-preserves-host-equivalent-execution.md) → [0053](decisions/0053-plugins-declare-managed-workloads.md) → [Computer 插件与 Workload 合同](design/computer-plugin-workload-task-contract.md) → [容器与 Linux 主机运行适配设计](design/akashic-container-cloud-runtime-adaptation.md) → [Core 与 Host Bridge 安装设计](design/akashic-core-bridge-installer.md) → [非迁移实验合同](design/akashic-container-host-bridge-experiment-contract.md) → [Unified Shell Execution 设计](design/unified-shell-execution.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/plugin_composition/`、`agent/plugins/`、Workload Controller、exact-commit 安装、runtime identity、Supervisor 与隔离实验;正式 profile 迁移前先运行 plan-only 清单并取得独立批准 | | Provider、模型角色、运行时切换、usage、首次配置、模型普通插件化 | `projectneed` RUN-005~RUN-012、ONB-001、CTX-001 → [0050](decisions/0050-model-revision-lives-in-ordinary-plugin.md) → [0054](decisions/0054-model-sync-refreshes-public-capabilities.md) → [模型普通插件与 Provider 组合规格](design/model-plugin-ordinary-capability-spec.md) → [0027](decisions/0027-runtime-models-use-generation-leases.md) → [0028](decisions/0028-model-credentials-live-with-workspace-connections.md) → [现行实现与历史验收基线](design/runtime-model-registry-and-onboarding.md) → [持久化状态地图](design/persistence-state-map.md) | `plugins/models/`、`plugins/opencode_go/`、`agent/plugin_composition/`、`agent/model_runtime/`、`agent/provider.py`、`bootstrap/settings_api.py`、`frontend/chat/src` | -| 插件安装、热重载、自验证、Cordis 迁移、plugin-data、Skill、Drift skill、MCP | `projectneed` 第 6、9~13 节 → [0008](decisions/0008-plugin-runtime-publishes-only-committed-snapshots.md) → [0024](decisions/0024-plugin-self-validation-uses-stable-and-latest.md) → [0026](decisions/0026-plugin-rollout-is-owned-by-the-parent-turn.md) → [插件自更新复杂度审查](design/plugin-update-entropy-audit.md) → [0036](decisions/0036-plugin-composition-keeps-promotion-owner.md) → [0038](decisions/0038-operator-trust-can-publish-offline-plugin-batches.md) → [0042](decisions/0042-plugin-diagnostics-preserve-domain-owners.md) → [0046](decisions/0046-plugin-candidate-validation-is-incremental.md) → [插件 install/uninstall/revert turn 边界发布合同](design/plugin-install-uninstall-turn-boundary-rollout.md) → [插件递归自验证运行时设计](design/recursive-plugin-self-validation.md) → [Cordis 插件迁移能力等价验收](design/cordis-plugin-capability-parity.md) → [插件 v3 最终迁移地图](design/plugin-v3-final-migration-map.md) → [插件 v3 生产替代清单](design/plugin-v3-production-readiness-checklist.md) → [插件 v3 admission/lifecycle 收口合同](design/plugin-v3-admission-lifecycle-closeout-task-contract.md) → [插件 v3 generation metadata 收口合同](design/plugin-v3-generation-metadata-task-contract.md) → [插件 v3 Runtime Inspection 合同](design/plugin-v3-runtime-inspection-task-contract.md) → [插件 v3 committed command catalog 合同](design/plugin-v3-command-catalog-task-contract.md) → [插件组合内核第一阶段任务合同](design/plugin-composition-kernel-task-contract.md) → [插件事件与同步执行能力任务合同](design/plugin-event-executor-task-contract.md) → [插件 TopologyView 任务合同](design/plugin-topology-view-task-contract.md) → [插件 lifecycle 接入点任务合同](design/plugin-lifecycle-seam-task-contract.md) → [Turn committed typed event 合同](design/plugin-turn-committed-event-task-contract.md) → [插件 v3 generation loader 任务合同](design/plugin-v3-loader-task-contract.md) → [插件 stable 原子组装任务合同](design/plugin-stable-atomic-assembly-task-contract.md) → [插件 candidate Root 隔离任务合同](design/plugin-candidate-root-isolation-task-contract.md) → [插件组合结构身份与 revision 任务合同](design/plugin-composition-revision-task-contract.md) → [插件组合 Health/Incident/Validation 任务合同](design/plugin-composition-health-incident-task-contract.md) → [插件 Transform/Observe 事件任务合同](design/plugin-transform-observe-task-contract.md) → [插件 generation 数据根任务合同](design/plugin-data-root-task-contract.md) → [插件 Tool 组合事件任务合同](design/plugin-tool-composition-events-task-contract.md) → [插件 Tool v3 迁移组合 Gate 任务合同](design/plugin-tool-v3-migration-gate-task-contract.md) → [Citation + Meme 纯 v3 组合 Gate](design/plugin-passive-composition-v3-gate-task-contract.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/plugins/base.py`、`agent/plugins/install.py`、`agent/plugins/manager.py`、`agent/plugins/snapshot.py`、`agent/plugins/reload_journal.py`、`agent/plugins/turn_rollout.py`、`agent/plugins/skill_links.py`、`agent/control/runtime.py`、`agent/looping/core.py`、`agent/mcp/client.py`、`agent/plugin_composition/context.py`、`agent/plugin_composition/effect.py`、`bootstrap/app.py`、`utils/process_group.py` | +| 插件安装、热重载、自验证、Cordis 迁移、plugin-data、Skill、Drift skill、MCP | `projectneed` 第 6、9~13 节 → [0008](decisions/0008-plugin-runtime-publishes-only-committed-snapshots.md) → [0024](decisions/0024-plugin-self-validation-uses-stable-and-latest.md) → [0026](decisions/0026-plugin-rollout-is-owned-by-the-parent-turn.md) → [插件自更新复杂度审查](design/plugin-update-entropy-audit.md) → [0036](decisions/0036-plugin-composition-keeps-promotion-owner.md) → [0038](decisions/0038-operator-trust-can-publish-offline-plugin-batches.md) → [0042](decisions/0042-plugin-diagnostics-preserve-domain-owners.md) → [0046](decisions/0046-plugin-candidate-validation-is-incremental.md) → [插件 install/uninstall/revert turn 边界发布合同](design/plugin-install-uninstall-turn-boundary-rollout.md) → [插件递归自验证运行时设计](design/recursive-plugin-self-validation.md) → [Cordis 插件迁移能力等价验收](design/cordis-plugin-capability-parity.md) → [插件 v3 最终迁移地图(历史)](design/plugin-v3-final-migration-map.md) → [插件 v3 生产替代清单(历史)](design/plugin-v3-production-readiness-checklist.md) → [插件 v3 admission/lifecycle 收口合同](design/plugin-v3-admission-lifecycle-closeout-task-contract.md) → [插件 v3 generation metadata 收口合同](design/plugin-v3-generation-metadata-task-contract.md) → [插件 v3 Runtime Inspection 合同](design/plugin-v3-runtime-inspection-task-contract.md) → [插件 v3 committed command catalog 合同](design/plugin-v3-command-catalog-task-contract.md) → [插件组合内核第一阶段任务合同](design/plugin-composition-kernel-task-contract.md) → [插件事件与同步执行能力任务合同](design/plugin-event-executor-task-contract.md) → [插件 TopologyView 任务合同](design/plugin-topology-view-task-contract.md) → [插件 lifecycle 接入点任务合同](design/plugin-lifecycle-seam-task-contract.md) → [Turn committed typed event 合同](design/plugin-turn-committed-event-task-contract.md) → [插件 v3 generation loader 任务合同](design/plugin-v3-loader-task-contract.md) → [插件 stable 原子组装任务合同](design/plugin-stable-atomic-assembly-task-contract.md) → [插件 candidate Root 隔离任务合同](design/plugin-candidate-root-isolation-task-contract.md) → [插件组合结构身份与 revision 任务合同](design/plugin-composition-revision-task-contract.md) → [插件组合 Health/Incident/Validation 任务合同](design/plugin-composition-health-incident-task-contract.md) → [插件 Transform/Observe 事件任务合同](design/plugin-transform-observe-task-contract.md) → [插件 generation 数据根任务合同](design/plugin-data-root-task-contract.md) → [插件 Tool 组合事件任务合同](design/plugin-tool-composition-events-task-contract.md) → [插件 Tool v3 迁移组合 Gate 任务合同](design/plugin-tool-v3-migration-gate-task-contract.md) → [Citation + Meme 纯 v3 组合 Gate(历史)](design/plugin-passive-composition-v3-gate-task-contract.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/plugins/base.py`、`agent/plugins/install.py`、`agent/plugins/manager.py`、`agent/plugins/snapshot.py`、`agent/plugins/reload_journal.py`、`agent/plugins/turn_rollout.py`、`agent/plugins/skill_links.py`、`agent/control/runtime.py`、`agent/looping/core.py`、`agent/mcp/client.py`、`agent/plugin_composition/context.py`、`agent/plugin_composition/effect.py`、`bootstrap/app.py`、`utils/process_group.py` | | Core 领域 Observe 事件 | [Core 领域 Observe 事件合同](design/plugin-domain-observe-events-task-contract.md) → [插件 Transform 与 Observe 事件任务合同](design/plugin-transform-observe-task-contract.md) | `agent/turn_events/observe.py`、`agent/lifecycle/composition.py`、`bus/event_bus.py`、`agent/retrieval/default_pipeline.py` | | 插件 v3 包级 Skill/Drift skill/Dashboard 声明 | [插件 v3 generation loader 任务合同](design/plugin-v3-loader-task-contract.md) → [插件 v3 包级 contribution 任务合同](design/plugin-v3-package-contributions-task-contract.md) | `agent/plugins/composable.py`、`agent/plugins/manager.py`、`agent/plugins/generation.py` | | 插件只读既有 Session 投影 | [持久化状态地图](design/persistence-state-map.md) → [插件 Session Read 组合能力任务合同](design/plugin-session-read-service-task-contract.md) | `agent/plugin_composition/session_read.py`、`agent/plugins/manager.py`、`session/manager.py` | | 插件 v3 Dashboard 注册与数据边界 | [插件 v3 包级 contribution 任务合同](design/plugin-v3-package-contributions-task-contract.md) → [v3 DashboardContext 任务合同](design/plugin-v3-dashboard-context-task-contract.md) | `agent/plugin_composition/dashboard.py`、`agent/plugins/dashboard_host.py` | | 插件 v3 静态投影与 exact Root runtime | [v3 DashboardContext 任务合同](design/plugin-v3-dashboard-context-task-contract.md) → [静态投影与 exact runtime 任务合同](design/plugin-v3-static-projection-runtime-task-contract.md) | `agent/plugin_composition/model.py`、`agent/plugins/composable.py`、`agent/plugins/snapshot.py`、`agent/plugins/dashboard_host.py` | | Akasha v3、feedback、Inspector 与 Mobile recall | [持久化状态地图](design/persistence-state-map.md) → [Akasha 在线与重放](design/akasha-v2-runtime-migration.md) → [Akasha v3 迁移任务合同](design/akasha-plugin-v3-migration-task-contract.md) | `plugins/akasha/`、`core/memory/plugin.py`、`agent/plugin_composition/runtime_services.py`、`agent/plugins/manager.py` | -| Citation/Meme v3 被动回复组合接入点 | [持久化状态地图](design/persistence-state-map.md) → [lifecycle 接入点任务合同](design/plugin-lifecycle-seam-task-contract.md) → [candidate Root 隔离任务合同](design/plugin-candidate-root-isolation-task-contract.md) → [v3 被动回复组合接入点任务合同](design/plugin-v3-passive-response-seams-task-contract.md) → [纯 v3 组合 Gate](design/plugin-passive-composition-v3-gate-task-contract.md) → [WebUI E2E Gate](design/plugin-passive-webui-v3-e2e-task-contract.md) | `agent/lifecycle/types.py`、`agent/lifecycle/phases/after_reasoning.py`、`agent/plugin_composition/model.py`、`agent/plugins/composable.py`、`agent/plugins/manager.py`、`agent/plugins/dashboard_host.py`、`bootstrap/chat_api.py`、`bootstrap/web_shell.py` | +| Citation/Meme v3 被动回复组合接入点 | [持久化状态地图](design/persistence-state-map.md) → [lifecycle 接入点任务合同](design/plugin-lifecycle-seam-task-contract.md) → [candidate Root 隔离任务合同](design/plugin-candidate-root-isolation-task-contract.md) → [v3 被动回复组合接入点任务合同](design/plugin-v3-passive-response-seams-task-contract.md) → [纯 v3 组合 Gate(历史)](design/plugin-passive-composition-v3-gate-task-contract.md) → [WebUI E2E Gate](design/plugin-passive-webui-v3-e2e-task-contract.md) | `agent/lifecycle/types.py`、`agent/lifecycle/phases/after_reasoning.py`、`agent/plugin_composition/model.py`、`agent/plugins/composable.py`、`agent/plugins/manager.py`、`agent/plugins/dashboard_host.py`、`bootstrap/chat_api.py`、`bootstrap/web_shell.py` | | 插件 v3 prepared context、只读 Memory runtime 与显式 interaction 撤销 | [插件 lifecycle 接入点任务合同](design/plugin-lifecycle-seam-task-contract.md) → [context-prepared 与 Memory capability 任务合同](design/plugin-context-prepared-memory-capability-task-contract.md) → [Plugin Undo v3 与 interaction 撤销协调合同](design/plugin-v3-interaction-undo-task-contract.md) | `agent/lifecycle/composition.py`、`agent/lifecycle/phases/before_turn.py`、`agent/plugin_composition/runtime_services.py`、`agent/plugin_composition/interaction_undo.py`、`agent/plugins/interaction_undo.py`、`agent/plugins/manager.py`、`session/store.py` | | 插件 v3 background job、Agent Work 与 LLM capability | [插件 v3 Proactive / background job 历史合同](design/plugin-v3-proactive-jobs-task-contract.md) → [插件 v3 Agent Work 能力合同](design/plugin-v3-agent-work-capability-task-contract.md) → [插件 v3 MCP/managed process 合同](design/plugin-v3-mcp-managed-process-task-contract.md) → [持久化状态地图](design/persistence-state-map.md) | `agent/plugin_composition/background_jobs.py`、`agent/plugins/generation_job_host.py`、`agent/tools/registry.py`、`agent/plugins/manager.py`、`agent/control/`、`bootstrap/control_execution.py` | | 插件 v3 inbound/outbound channel capability | [插件 v3 Channel capability 合同](design/plugin-v3-channel-capability-task-contract.md) → [插件 v3 Channel 附件持久化合同](design/plugin-v3-channel-attachment-task-contract.md) → [插件 v3 committed command catalog 合同](design/plugin-v3-command-catalog-task-contract.md) | `agent/plugin_composition/`、`agent/plugins/manager.py`、`agent/plugins/snapshot.py`、`agent/tools/message_push.py`、`agent/looping/core.py`、`agent/core/passive_turn.py`、`agent/lifecycle/phases/after_turn.py`、`agent/turns/orchestrator.py`、`agent/turns/outbound.py`、`bootstrap/app.py`、`bootstrap/channel_host.py`、`bootstrap/channels.py`、`bootstrap/passive_worker.py`、`bus/queue.py`、`bus/events.py`、`infra/channels/base.py`、`infra/channels/contract.py`、`infra/channels/delivery.py`、`infra/channels/telegram_channel.py`、`infra/channels/qq_channel.py`、`infra/channels/web_chat_channel.py`、`infra/mobile_realtime/channel.py`、`session/manager.py`、`session/store.py`、`/mnt/data/coding/akashic-plugin/feishu`、`/mnt/data/coding/akashic-plugin/qqbot` | | 移动端查看 Markdown、定时任务、插件、Skill、MCP | `projectneed` 第 6、10~13 节 → [移动端运行时检查](design/mobile-runtime-inspection.md) → [v3 Mobile UI/query capability](design/plugin-v3-mobile-ui-query-task-contract.md) → [持久化状态地图](design/persistence-state-map.md) | `infra/mobile_realtime/runtime_inspection.py`、`infra/mobile_realtime/protocol.py`、`infra/mobile_realtime/channel.py`、`agent/plugins/mobile_ui.py` | | Workspace、配置、凭据、迁移、备份 | `projectneed` 第 6、11~13 节 → [持久化状态地图](design/persistence-state-map.md) → [0021](decisions/0021-yoyo-workspace-ledger-defines-migration-origin.md) → [Yoyo 迁移维护手册](design/git-migration-authoring.md) | `main.py`、`bootstrap/init_workspace.py`、`agent/config.py`、`agent/migrations/`、`migrations/yoyo/`、`agent/model_runtime/auth/store.py`、`scripts/rolling_backup.py` | | 高风险 refactor、语义不变重构、CI oracle | `projectneed` 第 4~6、13、15 节 → [综合重构账本](refactor/clean-code-ledger.md) → [上下文事故设计](design/project-workbook-and-semantic-safety.md) → 相关决策 | 改动前后的完整 diff、semantic tests、write set、故障注入 | -| 变更影响 Gate、跨仓库插件契约 | `projectneed` 第 10、13、15 节 → [0004](decisions/0004-cross-repository-evidence-is-an-immutable-combination.md) → [移动端与跨仓库 Gate](design/mobile-cross-repository-semantic-gate.md) → [Gate 总体设计](spark/2026-07-16-change-impact-contract-gate.md) → [持久化状态地图](design/persistence-state-map.md) | `tests_scenarios/contracts/`、`docker/debug/gate.py`、`private_runtime/` | +| 变更影响 Gate、跨仓库插件契约 | `projectneed` 第 10、13、15 节 → [0004](decisions/0004-cross-repository-evidence-is-an-immutable-combination.md) → [移动端与跨仓库 Gate](design/mobile-cross-repository-semantic-gate.md) → [Gate 总体设计](spark/2026-07-16-change-impact-contract-gate.md) → [测试与 Gate 清理账本](refactor/test-gate-cleanup-ledger.md) → [持久化状态地图](design/persistence-state-map.md) | `tests_scenarios/contracts/`、`docker/debug/gate.py`、`private_runtime/` | | Companion 安全、容量和长时运行 Edge Case | `projectneed` SEC-001~SEC-010 → [0017](decisions/0017-one-person-companion-security-boundary.md) → [Companion 安全边界与 Edge Case 实施设计](design/security-scan-edge-cases.md) → [持久化状态地图](design/persistence-state-map.md) | 相关 D1~D9 owner、`tests_scenarios/contracts/`、`docker/debug/gate.py` | | Harness benchmark、独立 runtime trial、证据驱动优化 | `projectneed` 第 8~13、15 节 → [V4 Flash Harness Benchmark 设计](spark/2026-07-30-v4flash-harness-benchmark-design.md) → [Benchmark 诊断循环设计](spark/2026-07-30-agent-benchmark-diagnostic-loop-design.md) → [0010](decisions/0010-provider-default-output-and-benchmark-diagnostics.md) → [0011](decisions/0011-benchmark-concurrency-six.md) → [实验 ledger](benchmark/v4flash-harness-experiment-ledger.md) → [运行审计](benchmark/terminalbench-2.1-run-audit-2026-08-05.md) → [逐题 CSV](benchmark/terminalbench-2.1-case-results-2026-08-05.csv) → [持久化状态地图](design/persistence-state-map.md) | `benchmark/harbor_v4flash/`、`agent/control/`、`bootstrap/control_execution.py`、`docker/debug/`、独立 artifact store 与 experiment ledger | | Shell、长任务、PTY、进程续接或轮询 | `projectneed` SH-001、RUN-002~RUN-003、ERR-001 → [0014](decisions/0014-shell-uses-unified-execution.md) → [Unified Shell Execution 设计](design/unified-shell-execution.md) | `agent/tools/shell.py`、`agent/tools/unified_exec.py`、`agent/tools/meta/register.py`、`agent/background/subagent_profiles.py`、`bootstrap/tools.py` | diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 20a732e69..1b117cd33 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -101,6 +101,8 @@ Git worktree 保存源码、测试和项目文档。Akashic `` 保存 ## 5. Gate +测试只固定现实可观察的回归、非平凡不变量或边界,以及具体 bug。代码发生变化或覆盖率提高本身不是新增测试的理由。优先复用行为边界上的既有覆盖,不测试字面量、映射、显然控制流、实现细节或已经删除的能力;只有“能力不存在”本身是合同时才验证其缺失。并发测试在实际可行时使用确定性协调或受控调度,不用 sleep 猜测时序。 + 完成相关测试和静态检查后运行: ```bash @@ -111,6 +113,8 @@ Gate 根据 Git diff 选择场景,并把报告写入 `docker/debug/reports/cha 生产路径与受保护合同同时变化时,Gate 必须扩大为完整公开场景执行,不能以结构性拒绝代替验证。测试失败先归因为实现、环境或契约冲突;修改断言、跳过场景和缩减 Gate 需要独立理由与授权。 +仓库保留 1080 项 Python 回归和 62 项 Web 回归。普通 Pull Request 运行全部保留测试与 change-impact Gate;`scripts/check_test_budget.py` 会拒绝数量偏离、无效清单或藏在 `tests_scenarios/contracts/retained-test-files.txt` 外的测试文件。插件候选运行手动 `Plugin v3 Candidate Gates` workflow 的 fleet completeness、Mobile 和公共 WebUI。正式发布所需的真实 workspace 演练由拥有部署输入的发布流程负责,仓库 CI 不伪造该证据。删除范围、保留理由、已知取舍与恢复点见[测试与 Gate 清理账本](refactor/test-gate-cleanup-ledger.md)。 + ## 6. Review 模式 纯评审任务走 `Read → Ownership → Review → Deliver`,默认只读,不创建实现分支、不修改候选代码,也不把发现自动写入 GitHub。用户要求修复、发表评论或更新工作手册时,重新建立相应写入合同。 diff --git a/docs/decisions/0037-plugin-runtime-is-pure-v3.md b/docs/decisions/0037-plugin-runtime-is-pure-v3.md index 1b539aa65..35221602e 100644 --- a/docs/decisions/0037-plugin-runtime-is-pure-v3.md +++ b/docs/decisions/0037-plugin-runtime-is-pure-v3.md @@ -6,6 +6,8 @@ - supersedes:[0008](0008-plugin-runtime-publishes-only-committed-snapshots.md) 的 API v2 与 legacy host 选择 - superseded by:无 +> 2026-09-02 对账:本决策的 pure-v3 runtime 结论仍有效;下文 E1~E4 是当时的迁移验收计划,已被当前 fleet、Mobile、公共 WebUI 候选 Gate 和发布流程拥有的真实环境验收取代。E1/E2 固定的 API 已随 v2 compatibility 删除,E4 又依赖不存在的 E3 runner,不能继续作为当前合并条件。 + ## 背景 API v2 曾用 `prepare/activate/retire/terminate` 与固定贡献字段建立第一版原子发布。 @@ -26,9 +28,9 @@ Core 拥有 artifact、candidate、stable/latest、lease、journal、晋升与 v2 consumer 迁走后立即删除对应 legacy owner,不保留 deprecated alias 或空壳。 3. Computer Use Linux 与 Context Pressure 退出已跟踪 fleet。卸载只移除安装清单与 能力 cache;既有 `plugin-data` 默认保留,不因代码收敛而物理删除。 -4. 代码合并与 hua-home 正式替换分开。只有同一 clean head 上的 static fleet、Mobile、 - WebUI、Tool/Passive composition 以及分组 E1~E4 报告全部通过,才能声明为线上替换 - candidate。正式 workspace 的备份、切换和回滚仍需单独授权。 +4. 代码合并与 hua-home 正式替换分开。同一 clean head 必须通过 fleet source/API compatibility、 + Mobile 与公共 WebUI 候选 Gate;正式 workspace 的备份、真实环境验收、切换和回滚由发布 + 流程拥有并仍需单独授权。 ```text 外部插件 source @@ -72,8 +74,8 @@ Core 拥有 artifact、candidate、stable/latest、lease、journal、晋升与 lifecycle、固定贡献 consumer、phase module 注入口或 EventBus-to-V3 类型桥。 - 每个领域完成 candidate discard/promote、old lease drain、Effect/resource cleanup、进程内失败与 子进程崩溃恢复;不为断电或物理停机扩张本轮范围。 -- 同一 clean Core head 运行 static fleet、Mobile、Tool/Passive composition、WebUI 与 E1~E4; - E4 从前三组报告定位身份并证明复制 workspace 中 `sessions.db/messages`、memory、 - plugin-data、artifact 与 pointer 的受保护摘要不发生未授权变化。 +- 同一 clean Core head 运行 fleet source/API compatibility、Mobile 与公共 WebUI;发布流程 + 另外用真实部署输入证明 `sessions.db/messages`、memory、plugin-data、artifact 与 pointer + 不发生未授权变化。 - 完成状态必须由测试和 Gate 报告确认;还有 v2 consumer 、blocked scenario 或非同 head 证据时不得声称 pure-v3 ready。 diff --git a/docs/design/akasha-plugin-v3-migration-task-contract.md b/docs/design/akasha-plugin-v3-migration-task-contract.md index aded6641b..39c4bda41 100644 --- a/docs/design/akasha-plugin-v3-migration-task-contract.md +++ b/docs/design/akasha-plugin-v3-migration-task-contract.md @@ -69,7 +69,8 @@ pending user row Akasha sidecars - Core 进程崩溃:重开 engine/Manager 后只从已提交 Session rows 与 sidecar 恢复;未提交 staged marker 不出现,已提交 Inspector/recall 等价。 - 不扩展到任意断电时点或停机 checkpoint;SQLite/现有 sidecar 发布协议继续拥有自己的 durability。 -- 最终 E1/E4 在一次 copied-workspace Gate 中验证 feedback、active/persisted recall、Dashboard、Mobile、 - sidecar hash、SessionDB append-only 与 cleanup;本任务不写正式 workspace。 +- 当前候选 Gate 验证 Mobile 与公共插件边界;正式发布流程在获授权的 workspace 副本上验证 + feedback、active/persisted recall、Dashboard、sidecar hash、SessionDB append-only 与 cleanup。 + 本任务不写正式 workspace。 恢复点:`backup/akasha-plugin-v3-pre-20260817`。 diff --git a/docs/design/plugin-candidate-root-isolation-task-contract.md b/docs/design/plugin-candidate-root-isolation-task-contract.md index 2a86c8650..2c29c7864 100644 --- a/docs/design/plugin-candidate-root-isolation-task-contract.md +++ b/docs/design/plugin-candidate-root-isolation-task-contract.md @@ -57,7 +57,7 @@ Core 提供隔离路径和生命周期 owner,不解释插件领域数据,也 - direct candidate invariant 失败不得执行 formal apply;下一次 fresh attempt 可以独立成功。 - installed promotion 在 Skill projection 或 owner commit 失败后,latest、production Root 和正式候选 owner 必须全部清除,stable pointer/Root/data 保持原值。 - v2-only candidate promotion 产生新 stable snapshot,但继续复用未变化的旧 stable Root;candidate clone module 与 attempt data 不进入 stable。 -- targeted:`tests/test_plugin_composition_loader.py`、`tests/test_plugin_hot_reload.py`。 +- targeted:`tests/test_plugin_composition_lifecycle.py`、`tests/test_plugin_hot_reload.py`。旧 loader 细分测试已在 2026-09-02 测试预算清理中移除。 - cumulative:manager/runtime control/composition kernel 与公开 change Gate。 - 本地证据:composition loader/kernel/hot reload `188 passed`;manager/runtime control/reload journal/turn rollout/skill links/source/install 与 composition events/executor/lifecycle/experiment `159 passed`;Basedpyright `0 errors`,`git diff --check` 通过。 - Terra xhigh 只读复审无 P0;其 promotion 失败、partial mount cancellation 和 registry cleanup findings 已转成上述 oracle。 diff --git a/docs/design/plugin-event-executor-task-contract.md b/docs/design/plugin-event-executor-task-contract.md index 3d6a31da4..63fcc6c81 100644 --- a/docs/design/plugin-event-executor-task-contract.md +++ b/docs/design/plugin-event-executor-task-contract.md @@ -4,6 +4,7 @@ - 负责范围:组合内核的 typed event、Fiber-owned listener/task、受限同步并发执行服务、验证回执和隔离测试。 - 当前阶段:complete +- 证据状态:historical;2026-09-02 已删除本任务的内部 events/executor/kernel 测试。E1/E2 也因随后删除的 v2 组合 API 而失效;当前合同由保留的 plugin lifecycle 与 hot reload 行为回归承担。 ## Goal @@ -60,9 +61,8 @@ protected_state: - current RuntimeSnapshot publication semantics allowed_paths: - agent/plugin_composition/** - - tests/test_plugin_composition_events.py - - tests/test_plugin_composition_executor.py - - tests/test_plugin_composition_kernel.py + - tests/test_plugin_composition_lifecycle.py + - tests/test_plugin_hot_reload.py - tests_scenarios/contracts/impact.toml - tests_scenarios/contracts/coverage-baseline.json - docs/projectneed.md diff --git a/docs/design/plugin-install-uninstall-turn-boundary-rollout.md b/docs/design/plugin-install-uninstall-turn-boundary-rollout.md index b59b7bca7..9c3bca67d 100644 --- a/docs/design/plugin-install-uninstall-turn-boundary-rollout.md +++ b/docs/design/plugin-install-uninstall-turn-boundary-rollout.md @@ -304,7 +304,7 @@ allowed_paths: - "skills/develop-akashic-plugin/**" - "skills/plugin-system/**" - "tests/test_plugin_*.py" - - "tests/test_channel_host.py" + - "tests/test_message_bus_admission.py" - "tests/test_builtin_*plugin*.py" - "tests/control/**" - "tests/semantic/**" diff --git a/docs/design/plugin-passive-composition-v3-gate-task-contract.md b/docs/design/plugin-passive-composition-v3-gate-task-contract.md index c47b55c5c..0e6951adb 100644 --- a/docs/design/plugin-passive-composition-v3-gate-task-contract.md +++ b/docs/design/plugin-passive-composition-v3-gate-task-contract.md @@ -1,5 +1,7 @@ # Citation + Meme 纯 v3 组合 Gate 任务合同 +> 历史任务合同:对应 runner 已在 2026-09-02 退出独立 CI Gate,但文件仍作为公共 WebUI Gate 的 exact source、装配和摘要 helper。内部 listener/snapshot 排列不再单独运行;当前依据见[测试与 Gate 清理账本](../refactor/test-gate-cleanup-ledger.md)。 + ## 1. 目标 用一个可公开复现的跨仓 Gate 证明 Citation 与 Meme 在删除 v2 shell 后,仍能通过 @@ -57,10 +59,4 @@ public v3 contract ──► PluginManager.load_all ──► stable snapshot le ## 5. 验收 -```bash -python docker/debug/plugin_passive_composition_v3_gate.py --require-clean-core -python -m basedpyright --level error docker/debug/plugin_passive_composition_v3_gate.py -git diff --check -``` - -真实 Gate 的 `gate.json` 必须 `status=passed`;命令失败或报告缺失都不能称为迁移成功。 +当时的验收要求是 runner 报告 `status=passed`、静态检查和 `git diff --check` 通过;当前不再把该历史报告作为迁移或合并条件。 diff --git a/docs/design/plugin-stable-atomic-assembly-task-contract.md b/docs/design/plugin-stable-atomic-assembly-task-contract.md index 7443f0e5e..deb5317e1 100644 --- a/docs/design/plugin-stable-atomic-assembly-task-contract.md +++ b/docs/design/plugin-stable-atomic-assembly-task-contract.md @@ -66,7 +66,7 @@ v2 不是新组合平面的长期成员。本 PR 中下列代码只承担迁移 - required Service 永不出现时 fail-loud;`current_snapshot`、retained snapshot、active plugins/generations 和 scopes 都保持启动前状态。 - legacy prepare、tool catalog 或注册失败时调用 terminate/Scope cleanup,失败插件不残留,剩余插件从全新对象重建。 - 连续取消不能截断批次清理;全部 Scope、Root effect、KV rollback 和 topology Skill catalog 完成后才向调用方恢复 `CancelledError`。 -- targeted:`tests/test_plugin_composition_loader.py`、`tests/test_plugin_manager.py`。 +- targeted:`tests/test_plugin_composition_lifecycle.py`、`tests/test_plugin_hot_reload.py`。旧 loader/manager 细分测试已在 2026-09-02 测试预算清理中移除。 - cumulative:plugin hot reload/runtime control/snapshot/composition 全量相关测试、Basedpyright、`git diff --check`、公开 change Gate。 - 停止条件:正式 plugin-data 在失败批次中改变、Core-managed endpoint 提前开放、Root 被 snapshot lease 前释放、取消后残留 task/process/catalog、失败重试复用旧 instance。 - 回滚点:Git tag `backup/plugin-atomic-assembly-r2-before-20260815`。 diff --git a/docs/design/plugin-v3-final-migration-map.md b/docs/design/plugin-v3-final-migration-map.md index 57e765c48..3bdebb6a7 100644 --- a/docs/design/plugin-v3-final-migration-map.md +++ b/docs/design/plugin-v3-final-migration-map.md @@ -1,5 +1,7 @@ # 插件 v3 最终迁移地图 +> 历史迁移地图:E1~E4 是 2026-08 的分批计划,不是当前可执行 Gate。当前入口与删除依据分别见 [`docs/WORKFLOW.md`](../WORKFLOW.md) 和[测试与 Gate 清理账本](../refactor/test-gate-cleanup-ledger.md)。 + 本文记录 Issue [#394](https://github.com/kachofugetsu09/akashic-agent/issues/394) 这一轮 Cordis 风格插件改造的目标结构、当前实现栈、剩余迁移范围和 v2 物理删除顺序。 它是 2026-08-16 的实施接手点,不替代 diff --git a/docs/design/plugin-v3-mobile-ui-query-task-contract.md b/docs/design/plugin-v3-mobile-ui-query-task-contract.md index c4409325c..a742bbfdb 100644 --- a/docs/design/plugin-v3-mobile-ui-query-task-contract.md +++ b/docs/design/plugin-v3-mobile-ui-query-task-contract.md @@ -76,7 +76,8 @@ artifact/workspace、或 v2 provider 行为漂移都停止交付。 实现 head 的独立复核未发现 P0/P1。集成分支已运行 Mobile UI、Manager、lifecycle、loader、kernel 与 hot-reload 累计回归 `372 passed`;相关 Basedpyright 为 `0 errors`,compileall 与 `git diff --check` 通过。 -完整 E1/E4 仍由最终 exact plugin lock 统一执行,本合同不把定向回归写成生产替换证据。 +最终 exact plugin lock 由 fleet source/API compatibility 与 Mobile Gate 对账;正式 workspace +替换证据仍由拥有部署输入的发布流程负责,本合同不把定向回归写成生产替换证据。 ## 5. 回滚 diff --git a/docs/design/plugin-v3-production-readiness-checklist.md b/docs/design/plugin-v3-production-readiness-checklist.md index 58c20ab06..10f95f191 100644 --- a/docs/design/plugin-v3-production-readiness-checklist.md +++ b/docs/design/plugin-v3-production-readiness-checklist.md @@ -1,5 +1,7 @@ # 插件 v3 生产替代清单 +> 历史执行清单:其中 E1~E4 表格记录 2026-08 的迁移计划,不再是当前 CI 或发布命令。2026-09-02 的 Gate 去留与代码演进依据见[测试与 Gate 清理账本](../refactor/test-gate-cleanup-ledger.md);当前候选入口以 [`docs/WORKFLOW.md`](../WORKFLOW.md) 为准。 + 本文是 Issue [#394](https://github.com/kachofugetsu09/akashic-agent/issues/394) 的唯一执行清单。 [插件 v3 最终迁移地图](plugin-v3-final-migration-map.md)负责解释目标架构、现有 PR DAG 和删除顺序; 本文只记录每项能力是否已经具备可替代生产的证据。状态必须由实际 commit、测试和 Gate 推进, diff --git a/docs/design/project-workbook-and-semantic-safety.md b/docs/design/project-workbook-and-semantic-safety.md index f3c1983e5..7aa830a03 100644 --- a/docs/design/project-workbook-and-semantic-safety.md +++ b/docs/design/project-workbook-and-semantic-safety.md @@ -759,7 +759,7 @@ Phase 1 完整后锁住“绝不丢持久历史”。Phase 2 再收紧 runtime - `run_turn` retry 成功记录 `selected_plan`、`disabled_sections` 和 window,不再调用 `trim_history_async`。 - history window 小于原窗口时调用名称明确的 runtime-only mutator;它只修改内存中的 `session.messages` 和 `last_consolidated`,不刷新 `updated_at`,也不调用 store。 - `agent/looping/core.py::_assemble_passive_runtime` 不再把 `self.session_manager` 注入 reasoner。 -- `tests/test_safety_retry_service.py` 核对 dynamic-only 保持原 `session.messages`,50% history retry 只保留选中窗口。 +- `tests/test_session_compaction_runtime.py` 核对 runtime-only projection 不改写权威 Session,并固定 retry/commit 边界。旧 safety-retry 细分测试已在 2026-09-02 测试预算清理中移除。 - Phase 1 两个 case 开启对应的 runtime view 断言。 验收:`rg '_session_manager|trim_history_async' agent/core/passive_turn.py` 无匹配;prompt retry 的 protected store write set 为空;runtime view 的变化与 selected window 完全一致;retry trace 能解释本次发送了哪个窗口。 diff --git a/docs/design/react-core-scheduler-subagent-task-contract.md b/docs/design/react-core-scheduler-subagent-task-contract.md index 66e876446..6ebd312c2 100644 --- a/docs/design/react-core-scheduler-subagent-task-contract.md +++ b/docs/design/react-core-scheduler-subagent-task-contract.md @@ -169,7 +169,7 @@ allowed_paths: - bootstrap/tools.py - plugins/subagent/plugin.py - tests/control/test_scoped_turn.py - - tests/test_subagent_v3_shadow.py + - tests/semantic/test_recursive_plugin_self_validation_contract.py - docs/NOW.md - docs/design/react-core-scheduler-subagent.md - docs/design/react-core-scheduler-subagent-task-contract.md @@ -219,8 +219,8 @@ allowed_paths: - tests/**subagent** - tests/test_shell_tool.py - tests/test_plugin_hot_reload.py - - tests/test_plugin_packages.py - - tests/semantic/test_react_core_contract.py + - tests/test_plugin_generation_job_host.py + - tests/semantic/test_recursive_plugin_self_validation_contract.py - docs/NOW.md - docs/design/react-core-scheduler-subagent*.md forbidden_effects: diff --git a/docs/design/recursive-plugin-self-validation.md b/docs/design/recursive-plugin-self-validation.md index d2271aaa0..e81f94242 100644 --- a/docs/design/recursive-plugin-self-validation.md +++ b/docs/design/recursive-plugin-self-validation.md @@ -476,15 +476,13 @@ cache/// | 合同 | 直接证据 | |---|---| | latest/stable、install 完成定义、candidate 单 owner | `tests/test_plugin_runtime_control.py`、`tests/test_plugin_hot_reload.py` 的 selector、promotion、KV write 与 crash recovery 用例 | -| candidate 诊断入口 | `tests/test_plugin_doctor.py::test_plugin_doctor_reads_latest_artifact_candidate` 证明 doctor 按 pointer 读取 `.artifacts` 下的 latest | -| 跨 session 并发、同 session 串行 | `tests/test_turn_pipelines.py::test_process_direct_runs_concurrently_with_another_session`、`test_process_direct_waits_for_the_same_session_lane` | -| programmatic runtime、长 terminal、SessionDB 与默认 memory policy | `tests/control/test_exec_cli.py::test_exec_new_defaults_to_read_only_memory_and_selects_runtime`、`test_control_client_reads_terminal_larger_than_asyncio_default`、`tests/control/test_protocol.py::test_thread_runtime_selector_is_strict_and_inherited_by_turn` | -| `message_push` 不等父 session 且实际 send 串行 | `tests/test_support_modules.py::test_message_push_passive_role_does_not_wait_for_passive_lane`、`test_message_push_passive_role_serializes_actual_same_chat_send` | -| turn-local 调试投影 | `tests/test_support_modules.py::test_context_builder_debug_projection_is_turn_local` | -| 生产轨迹 oracle | `tests/semantic/test_recursive_plugin_self_validation_trajectory.py` 通过真实 `PluginManager` install、`ConversationRuntime` latest child、`DefaultReasoner`、候选工具、`message_push`、SessionDB、reload journal 与 promote 取证;stable misbinding、假 tool success、假领域结果从真实执行 seam 注入并必须被拒绝 | -| 聚合合同 oracle 与已知错误 | `tests/semantic/test_recursive_plugin_self_validation_contract.py` 对跨场景 observation 做稳定性自测:global lock、parent terminal overflow、semantic write、blocking push、crash promotion 等 mutant;它不替代生产轨迹测试 | - -上述证据注册为 P0 `recursive_plugin_validation` group、`plugin_runtime_selection` state contract 与 `recursive_plugin_self_validation_contract` scenario。Gate 的主通过证据由生产组件生成,不接受手工 observation:它观察 pointer/journal、真实 tool item、SessionDB、semantic write set、ChatLane timer 和 promote;独立 startup 用例覆盖 crash recovery。coverage baseline 只记录批准后的合同映射,不充当测试通过报告。 +| 跨 session 并发、同 session 排他 | `tests/control/test_conversation_runtime.py::test_runtime_executes_different_threads_concurrently`、`test_runtime_rejects_same_thread_input_and_interrupts_exact_turn` | +| programmatic runtime 与长 terminal | `tests/control/test_protocol.py`、`tests/control/test_control_execution.py` 的 selector、attached interrupt、metadata 与 terminal 边界 | +| `message_push` 和 channel finality | `tests/test_message_bus_admission.py` 的 passive turn、same-chat lane、provider receipt、取消与关闭用例 | +| 真实 lifecycle 边界 | `tests/test_plugin_runtime_control.py`、`tests/test_plugin_hot_reload.py`、`tests/control/test_conversation_runtime.py` 和 `tests/test_message_bus_admission.py` 组合观察 pointer、lease、session lane、terminal 与 crash recovery | +| 聚合合同 oracle 与已知错误 | `tests/semantic/test_recursive_plugin_self_validation_contract.py` 对 global lock、parent terminal overflow、semantic write、blocking push、crash promotion、假 tool item 和假领域结果 mutant 做稳定性自测 | + +上述保留证据注册为 P0 `recursive_plugin_validation` group、`plugin_runtime_selection` state contract 与 `recursive_plugin_self_validation_contract` scenario。Gate 的 mutant oracle 与 runtime/control 行为测试共同固定合同;coverage baseline 只记录批准后的合同映射,不充当测试通过报告。2026-09-02 删除的旧 trajectory、doctor、exec CLI 和 support 细分测试只可从 Git 历史查阅,不再是当前验收入口。 ### 14.7 真实模型闭环证据 diff --git a/docs/refactor/test-gate-cleanup-ledger.md b/docs/refactor/test-gate-cleanup-ledger.md new file mode 100644 index 000000000..9fb82180a --- /dev/null +++ b/docs/refactor/test-gate-cleanup-ledger.md @@ -0,0 +1,68 @@ +# 测试与 Gate 清理账本 + +本账本记录测试与 Gate 的永久收敛。数量只是预算,不是删除依据;取舍按用户可观察失败、持久化与安全边界、并发 finality、恢复能力和插件 v3 生命周期排序。 + +## 2026-09-02:保留最高价值的三分之一 + +### 结果 + +| 范围 | 清理前 | 保留 | 删除 | 预算结果 | +| --- | ---: | ---: | ---: | --- | +| Python | 3239 项 / 250 文件 | 1080 项 / 72 文件 | 2159 项 / 178 文件 | `ceil(3239 / 3) = 1080` | +| Node | 194 项 / 34 文件 | 62 项 / 4 文件 | 132 项 / 30 文件 | 低于三分之一 | +| PR CI job | 8 | 2 | 6 | 低于三分之一 | + +Python 的 1080 是仓库完整收集数,不是从完整套件中挑出的 PR 子集。`scripts/check_test_budget.py` 同时固定数量和文件集合;任何未列入 `tests_scenarios/contracts/retained-test-files.txt` 的新测试都会使 CI 失败。Node 只保留 mobile message state、pairing response schema、Web transport 和 Akasha mobile UI 四个行为边界,由唯一命令 `npm run test:web` 执行。 + +删除的精确路径以本次提交的 delete diff 为准。Python 删除清单 SHA-256 为 `e807c64144b4693959d85edd23bea2832832ad138e662cab752cd55c8a967785`,Node 删除清单 SHA-256 为 `9b6cc344774d16dbd7d4f9a4e2bc154c1c7285ef5434aaccfb905d786b1c01d1`;摘要基于排序后的仓库相对路径,每行一个。 + +### 保留理由 + +保留清单不是按文件大小或覆盖率生成。每个文件至少拥有以下一种高价值失败: + +- `tests/semantic/**`:P0 mutant/oracle、非破坏历史、模型 owner、递归插件验证和 change-impact Gate 自身的 fail-closed 合同。 +- `tests/control/**`、`test_session_store.py`、`test_message_bus_admission.py`:Turn admission、同 session 排他、跨 session 并发、中断、重放、终态一次性和消息只追加。 +- `test_plugin_hot_reload.py`、`test_plugin_install.py`、`test_plugin_generation_job_host.py`、`test_plugin_managed_process_host.py`、`test_plugin_runtime_control.py`、`test_plugin_turn_rollout.py`:插件 v3 generation、lease、promotion、rollback、卸载、进程清理和崩溃恢复。 +- `mobile_realtime/**`、`test_web_chat_channel.py`、`test_channel_attachment_store.py`、`test_durable_deliveries.py`:真实入口的认证、附件、游标、持久交付、跨客户端身份和 exactly-once/finality。 +- `test_context_compaction_contract.py`、`test_session_compaction_runtime.py` 及迁移测试:历史正文不得因裁切或迁移减少,迁移链必须 append-only 且可从旧状态恢复。单项迁移测试数量小,但保护不可逆数据变换。 +- `test_agent_restart.py`、`test_mcp_process_recovery.py`、`test_rolling_backup.py`、`test_runtime_smoke.py`:监听器归属、子进程 epoch、备份恢复和跨层启动/关闭失败语义。 +- `test_shell_tool.py`、`test_unified_exec.py`、`test_tool_executor.py`:外部进程、权限、取消和输出 finality 的信任边界。 +- `mobile-message-state.test.mjs`、`mobile-pairing.test.mjs`、`web-chat-transport.test.mjs`、`test_akasha_mobile_ui.mjs`:用户真正看到的消息身份、外部 pairing 响应校验、流式终态、草稿/阅读锚点和 Akasha 查询边界。 + +最后一次等额调整用 138 项更高价值边界替换 138 项内部覆盖:加入 rolling backup、MCP process recovery、attachment store、durable delivery、真实 Web ingress 和 runtime smoke;移出 MCP slot、turn pipeline、composition wiring、reload journal 以及重复的 mobile adapter/publisher 组合。数量不变,但对灾难恢复、进程恢复、权威附件、交付 finality 和真实入口的保护更强。 + +独立复审又完成两次等额交换:用 Web pairing 的外部响应 schema 边界替换一项通知文案字面测试;用 2 项正式 credential/ref 冻结与原始配置 revision drift 测试替换 2 项 injected requester wiring 测试。它们分别保护不可信网络输入和插件 secret/config 的 TOCTOU 边界,优先级高于展示字符串与依赖注入接线。 + +### 删除理由 + +被删除测试按主要理由归入以下类别。一个文件可能同时符合多项;删除仍有取舍,不声称它们完全没有价值。 + +| 删除类别 | 主要路径示例 | 为什么在 1080 预算外 | +| --- | --- | --- | +| 实现镜像与分层重复 | `test_agent_core_p*.py`、`test_plugin_composition_*.py`、`test_*_modules.py` | 固定 helper、slot、wiring、字段转发或显然控制流;同一可观察合同已在 runtime、control、generation 或 semantic 边界保留。 | +| 字面量、schema 与 catalog 枚举 | `test_plugin_static_manifest.py`、`test_plugin_config_schema.py`、`test_model_catalog_reader.py`、theme/module-boundary Node 测试 | 主要镜像常量、映射、导出列表或静态形状;真实加载、安装、协议拒绝或 UI 行为边界优先。 | +| 重复 adapter/client 组合 | `test_channel_base.py`、`test_channel_clients.py`、`test_core_channel_adapter.py`、mobile gateway/pairing/publisher 测试 | 相同身份、鉴权、交付和 publication 语义已由 Web/mobile 真实入口及持久存储边界覆盖。 | +| 已移除或历史过渡面 | `test_workspace_mcp_removed.py`、`test_plugin_v3_only_surface.py`、shadow/legacy migration 辅助面 | 仅证明旧入口不存在或过渡实现仍在;没有持续的公共 absence 合同则不占长期预算。真正不可逆的数据库迁移仍保留。 | +| 宽矩阵与低增量排列 | provider/model 普通安装组合、plugin composition 各 slot 组合、UI state 细分 Node 文件 | 多个用例沿同一路径只替换插件、provider 或状态枚举;保留最能穿过公共边界和失败路径的代表。 | +| benchmark、性能与部署演练 | `tests/benchmark/test_harbor_*.py`、WebUI performance `.test.mjs`、container/release rehearsal 测试 | 它们是专项测量或环境验收,不是每次源码变更都必须固定的核心回归;正式性能或发布验收应由独立、带真实环境证据的流程拥有。 | +| 被更高层 finality 覆盖 | `test_turn_pipelines.py`、`test_turn_effects.py`、`test_content_store.py`、部分 wake/drift 与 support 测试 | 保留 ConversationRuntime、SessionStore、durable delivery、semantic mutant 和 wake durable 边界,避免在下游重复验证同一 owner。 | + +主动放弃的检测粒度包括:每种 provider/plugin 的对称安装排列、每个 composition slot 的内部快照、全部桌面 UI 小状态、benchmark controller 细节,以及部分旧 CLI/部署 helper。若这些区域以后发生具体生产 bug,应优先在现有公共边界补一个回归,并从 1080 预算中移出更低价值测试,而不是扩大总数。 + +### Gate 清理 + +- 普通 PR 从 8 个 job 收敛到 `check-and-test` 与 `change-impact-gate` 两个。2026-07-18 引入的统一 Gate 已能按 diff 选择 P0 mutant/oracle 并对未知映射 fail closed,因此保留;它是当前 Core 变更的单一语义 owner。 +- 2026-07-14 的 control 三连跑和 restart soak、2026-08-18 的 static fleet、2026-08-15~16 的旧 composition 不再进入每个 PR。control/restart 已合并为一个每周 lifecycle job;旧 composition 已由当前 plugin lifecycle、hot reload 和可观察插件边界覆盖。 +- 手动候选 workflow 从 4 个 job 收敛到 1 个,只运行 fleet completeness、Mobile 和公共 WebUI。它们分别固定全部 18 个锁定插件的来源/v3-only/retired 排除、用户可见 Mobile ABI,以及 Citation/Meme 的真实公共 WebSocket 行为;不重复 1080/62 回归。 +- 2026-08-18 引入的 E1/E2 在 2026-09-02 Core 删除 v2 compatibility 后失效:锁定的 Emotion 仍导入已删除的 `CoreEvent`,Calendar 仍导入已删除的 `PROACTIVE_COMPONENTS`。这两条失败固定的是历史 API,不是当前可观察回归,因此删除 `plugin_v3_e1_gate.py` 与 `plugin_v3_e2_gate.py`,不通过升级外部插件来维持 Gate。E4 硬依赖 E1 报告和仓库中从未存在 runner 的 E3 报告,不能执行其发布合同,也删除 `plugin_v3_e4_gate.py`。恢复方式是 revert 本清理提交;若未来需要正式发布 rehearsal,应以当时的 Core、锁定插件和真实部署输入重新建立合同。 +- Terra 复审发现 static fleet 对旧锁会假阳性;因此 fleet lock 前移到 Calendar `048c8e8`、Emotion `d828fd7`、Observe `09214c2`、Feed `dccbcd9`、Fitbit `e0eda11` 与 Steam `d2ddd1b` 的当前正式 main,Mobile lock 同步其中的 Emotion、Observe 和 Fitbit。Fleet Gate 新增对所有生产源码 `from agent.* import ...` 的当前 Core export 检查,已删除符号会 fail closed;它只声称 source/API compatibility,不冒充完整运行加载或正式部署。 +- 2026-08-15 的 `plugin_composition_v3_gate.py` 只有历史文档调用者,并重复固定 Tool/plugin snapshot 排列,因此物理删除。`plugin_passive_composition_v3_gate.py` 不再作为独立 CI Gate,但公共 WebUI runner 真实复用它的 exact source、装配和摘要 helper;clean-head 验证暴露这一动态模块依赖后已恢复,避免为了删文件复制同一套逻辑。 +- `programmatic-control-nightly.yml` 改为每周唯一的 full-process lifecycle job,顺序运行 failure matrix、100-turn resource soak 与 restart soak;进程级 SIGTERM/crash、workspace lock 和资源泄漏因此仍有明确 owner,但不阻塞每个 PR。 +- semantic scenario 与 Content/Wake lock/H5 manifest 都只引用仍保留的测试;已删除的 slot、pipeline、gateway、shadow 和 support 测试不再被 Gate 间接复活。 +- 正式 workspace 演练不再由缺失前置报告的仓库脚本占位;未来需要时由拥有部署输入的发布流程重新建立可执行合同。本次清理不伪造发布通过。 + +### 恢复与验证 + +清理前恢复点:`/mnt/data/akasic-agent-backups/test-gate-one-third-20260902-before-clean/pre-hard-budget-71b27f5b.bundle`,SHA-256 `78c213310dc94c8ee5a16da65f8dd25c4dc0078aab7bb965cb772b91001ed7f5`。更早的完整测试归档为同目录 `test-and-gate-surface.tar.gz`。 + +本地验证:预算检查为 `python_files=72 python_tests=1080 node_files=4`;最终等额交换后的 Python 全量为 `1075 passed, 5 skipped`(155.87 秒),Node 为 `62 passed`。Python/测试/SDK Pyright、TypeScript、control schema、Yoyo append-only、SDK 11 项测试、workflow YAML、Gate audit 和 `git diff --check` 均通过;受保护合同变化触发的 27 个公开场景也通过。Terra xhigh 独立复审提出的 fleet coverage、pairing/credential swap、full-process lifecycle owner 和活跃文档悬空引用均已修正,代码与文档 P0/P1 清零。提交后仍需远端 CI 对精确 head 验证。 diff --git a/frontend/chat/src/browser-uuid.test.mjs b/frontend/chat/src/browser-uuid.test.mjs deleted file mode 100644 index 3625a5396..000000000 --- a/frontend/chat/src/browser-uuid.test.mjs +++ /dev/null @@ -1,35 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { createUuid, createUuidV7 } from "./browser-uuid.ts"; - -test("uses the native UUID implementation when the browser exposes it", () => { - const value = createUuid({ - randomUUID: () => "11111111-2222-4333-8444-555555555555", - getRandomValues: () => { throw new Error("fallback must not run"); }, - }); - assert.equal(value, "11111111-2222-4333-8444-555555555555"); -}); - -test("creates an RFC 4122 UUID with getRandomValues on plain HTTP", () => { - const value = createUuid({ - getRandomValues: (bytes) => { - bytes.fill(0xab); - return bytes; - }, - }); - assert.equal(value, "abababab-abab-4bab-abab-abababababab"); - assert.match(value, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u); -}); - -test("creates a cross-client UUIDv7 from browser time and randomness", () => { - const value = createUuidV7({ - getRandomValues: (bytes) => { - bytes.fill(0xab); - return bytes; - }, - }, 1_700_000_000_000); - - assert.equal(value, "018bcfe5-6800-7bab-abab-abababababab"); - assert.match(value, /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u); -}); diff --git a/frontend/chat/src/desktop-composer.test.mjs b/frontend/chat/src/desktop-composer.test.mjs deleted file mode 100644 index 5bfc93b08..000000000 --- a/frontend/chat/src/desktop-composer.test.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { nextComposerExpanded } from "./composer-layout.ts"; - -test("composer expansion stays owned by the draft until it is cleared", () => { - assert.equal(nextComposerExpanded(false, "short", () => false), false); - assert.equal(nextComposerExpanded(false, "wrapped draft", () => true), true); - - let measured = false; - assert.equal(nextComposerExpanded(true, "wrapped draft plus one", () => { - measured = true; - return false; - }), true); - assert.equal(measured, false); - assert.equal(nextComposerExpanded(true, "", () => true), false); -}); diff --git a/frontend/chat/src/desktop-conversation.test.mjs b/frontend/chat/src/desktop-conversation.test.mjs deleted file mode 100644 index df18a0da8..000000000 --- a/frontend/chat/src/desktop-conversation.test.mjs +++ /dev/null @@ -1,67 +0,0 @@ -import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; -import test from "node:test"; - -const conversation = await readFile(new URL("./desktop-conversation.tsx", import.meta.url), "utf8"); -const messageView = await readFile(new URL("./message-view.tsx", import.meta.url), "utf8"); -const styles = await readFile(new URL("./styles.css", import.meta.url), "utf8"); -const conversationShell = await readFile(new URL("./components/ai-elements/conversation.tsx", import.meta.url), "utf8"); -const desktopApp = await readFile(new URL("./desktop-chat-view.tsx", import.meta.url), "utf8"); -const desktopAutoScroll = await readFile(new URL("./desktop-auto-scroll.tsx", import.meta.url), "utf8"); - -test("desktop history isolates stable rows but never the active stream", () => { - assert.match(conversation, /message\.streaming === true \? "streaming" : "history-isolated"/); - assert.match(styles, /\.web-message-anchor\.history-isolated\s*\{[\s\S]*?content-visibility:\s*auto;/); - assert.doesNotMatch(styles, /\.web-message-anchor\.streaming\s*\{[\s\S]*?content-visibility/); - assert.match(conversationShell, /initial="instant"/); - assert.match(desktopApp, //); - assert.match(desktopApp, /scrollElement\.scrollTop \+= restoredAnchor\.getBoundingClientRect\(\)\.top - anchorTop/); -}); - -test("desktop plugin cards keep the control turn through terminal publication", () => { - assert.match( - conversation, - /const pluginTurnId = message\.controlTurnId;/, - ); - assert.equal((conversation.match(/turnId=\{pluginTurnId\}/g) ?? []).length, 3); -}); - -test("desktop auto-scroll subscribes only to the tail message and preserves user escape", () => { - assert.match(desktopApp, / { - assert.match(conversation, /deferRichContent/); - assert.match(conversation, /new IntersectionObserver/); - assert.match(conversation, /rootMargin: "800px 0px"/); - assert.match(messageView, /\{content\}<\/StaticMessageResponse>/); - assert.match(messageView, /features\.math \|\| features\.mermaid \|\| features\.code/); - assert.match(conversation, /enhancementSuspended=\{status !== "idle"\}/); -}); - -test("desktop static code fallback remains visually contained", () => { - assert.match(styles, /\.static-code-block\s*\{[\s\S]*?width:\s*fit-content;[\s\S]*?border:[^;]+;/); - assert.match(styles, /\.static-code-block\s*\{[\s\S]*?background:[^;]+;/); - assert.match(styles, /\.static-code-block\s*>\s*pre\s*\{[\s\S]*?overflow-x:\s*auto;/); -}); - -test("desktop reply availability uses one history index", () => { - assert.match(conversation, /new Set\(messages\.map\(\(message\) => message\.id\)\)/); - assert.match(conversation, /!messageIds\.has\(message\.reply\.messageId\)/); - assert.match(conversation, /stopScroll\(\);[\s\S]*?scrollIntoView\(\{ behavior: "instant", block: "center" \}\)/); - assert.doesNotMatch(conversation, /messages\.some/); -}); - -test("shared message contracts no longer import the desktop entry", async () => { - const sources = await Promise.all([ - "message-view.tsx", - "mobile-native.tsx", - "web-stream-projection.ts", - ].map((path) => readFile(new URL(`./${path}`, import.meta.url), "utf8"))); - for (const source of sources) assert.doesNotMatch(source, /from "\.\/main(?:\.tsx)?"/); -}); diff --git a/frontend/chat/src/kaomoji-markdown.test.mjs b/frontend/chat/src/kaomoji-markdown.test.mjs deleted file mode 100644 index a61c0fb30..000000000 --- a/frontend/chat/src/kaomoji-markdown.test.mjs +++ /dev/null @@ -1,199 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import kaomojiCollection from "kaomoji-collection/kaomoji.json" with { type: "json" }; -import { getMarkdown, parseMarkdownToStructure } from "stream-markdown-parser"; -import { configureKaomojiMarkdown, readKaomojiLiteral } from "./kaomoji-markdown.ts"; - -const FORMATTING_NODES = new Set([ - "emphasis", - "highlight", - "inline_code", - "insert", - "strong", - "strikethrough", - "subscript", - "superscript", -]); - -test("kaomoji stay literal without swallowing real Markdown", () => { - for (const value of [ - "(=^・・^=)", - "(*^▽^*)", - "(*´꒳`*)", - "(T_T)", - "^_^", - "*( ᵕ̤ᴗᵕ̤ )*", - "☆_.。.o(≧▽≦)o.。.:_☆", - "꒰*´∀`*꒱", - "ฅ^•ω•^ฅ", - ]) { - const markdown = configureKaomojiMarkdown(getMarkdown(`kaomoji-${value}`)); - const nodes = parseMarkdownToStructure(value, markdown, { final: false }); - assert.equal(hasFormattingNode(nodes), false, value); - assert.equal( - [...walkNodes(nodes)].some((node) => node.type === "kaomoji_literal" && node.content === value), - true, - value, - ); - } - - assert.equal(readKaomojiLiteral("(see *important*)", 0), undefined); - assert.equal(readKaomojiLiteral("(这是 *重点*)", 0), undefined); - for (const markdown of [ - "A --- B", - "x^2^", - "[link](https://example.com)", - "`code`", - "$x^2$", - "正文 (*^▽^*) **重点**", - "这是 *重点* ☆", - "中文 **重点** ٩", - "前缀 ~~删除~~ ♥", - "数学 *x* Ω", - "☆ ツ *シ* ☆", - "★ シ **ツ** ★", - "☆ ノ ~~ツ~~ ☆", - "☆_.。.o(≧", - "꒰*", - ]) { - assert.equal(readKaomojiLiteral(markdown, 0), undefined, markdown); - } - const prose = configureKaomojiMarkdown(getMarkdown("kaomoji-prose")); - assert.equal(hasNodeType(parseMarkdownToStructure("(see *important*)", prose, { final: true }), "emphasis"), true); - for (const [markdown, type] of [ - ["这是 *重点* ☆", "emphasis"], - ["中文 **重点** ٩", "strong"], - ["前缀 ~~删除~~ ♥", "strikethrough"], - ["数学 *x* Ω", "emphasis"], - ["☆ ツ *シ* ☆", "emphasis"], - ["★ シ **ツ** ★", "strong"], - ["☆ ノ ~~ツ~~ ☆", "strikethrough"], - ]) { - assert.equal(hasNodeType(parseMarkdownToStructure(markdown, prose, { final: true }), type), true, markdown); - } -}); - -test("kaomoji rule leaves code spans to Markdown", () => { - const markdown = configureKaomojiMarkdown(getMarkdown("kaomoji-code-spans")); - - for (const [source, code] of [ - ["`(*^▽^*)`", "(*^▽^*)"], - ["`` (*^▽^*) ``", "(*^▽^*)"], - ["prefix `(*^▽^*)` suffix", "(*^▽^*)"], - ["**bold** `(*^▽^*)` and (*^▽^*)", "(*^▽^*)"], - ]) { - for (const final of [false, true]) { - const nodes = parseMarkdownToStructure(source, markdown, { final }); - assert.deepEqual( - [...walkNodes(nodes)].filter((node) => node.type === "inline_code").map((node) => node.code), - [code], - source, - ); - assert.equal( - [...walkNodes(nodes)].some((node) => node.type === "kaomoji_literal" && node.content.includes("`")), - false, - source, - ); - } - } - - const fenced = parseMarkdownToStructure("```text\n(*^▽^*)\n```", markdown, { final: true }); - assert.equal(hasNodeType(fenced, "code_block"), true); - assert.equal(hasNodeType(fenced, "kaomoji_literal"), false); - - for (const final of [false, true]) { - const mixed = parseMarkdownToStructure("前缀 **重点** 后缀 (*^▽^*)", markdown, { final }); - assert.equal(hasNodeType(mixed, "strong"), true); - assert.equal(hasNodeType(mixed, "emphasis"), false); - assert.equal(hasNodeType(mixed, "superscript"), false); - } -}); - -test("kaomoji rule keeps Markstream append-tail parsing and stable nodes", () => { - const markdown = configureKaomojiMarkdown(getMarkdown("kaomoji-stream")); - const first = parseMarkdownToStructure("# stable\n\n(*", markdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - const second = parseMarkdownToStructure("# stable\n\n(*^▽^*)", markdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - - assert.strictEqual(second[0], first[0]); - assert.equal(markdown.stream?.stats?.().lastMode, "tail"); - assert.equal(hasFormattingNode(second), false); - - const decoratedMarkdown = configureKaomojiMarkdown(getMarkdown("kaomoji-decorated-stream")); - const decoratedPartial = parseMarkdownToStructure("# stable\n\n☆_.。.o(≧", decoratedMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - const decoratedComplete = parseMarkdownToStructure("# stable\n\n☆_.。.o(≧▽≦)o.。.:_☆", decoratedMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - const decoratedAppended = parseMarkdownToStructure("# stable\n\n☆_.。.o(≧▽≦)o.。.:_☆\n\nmore", decoratedMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - - assert.strictEqual(decoratedComplete[0], decoratedPartial[0]); - assert.strictEqual(decoratedAppended[0], decoratedComplete[0]); - assert.equal(decoratedMarkdown.stream?.stats?.().lastMode, "tail"); - - for (const delimiter of ["`", "``"]) { - const codeMarkdown = configureKaomojiMarkdown(getMarkdown(`kaomoji-code-stream-${delimiter.length}`)); - const partial = parseMarkdownToStructure(`# stable\n\n${delimiter}(*`, codeMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - const face = parseMarkdownToStructure(`# stable\n\n${delimiter}(*^▽^*)`, codeMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - const closed = parseMarkdownToStructure(`# stable\n\n${delimiter}(*^▽^*)${delimiter}`, codeMarkdown, { - final: false, - reuseStableTopLevelNodes: true, - }); - - assert.strictEqual(face[0], partial[0]); - assert.strictEqual(closed[0], face[0]); - assert.equal(hasNodeType(face, "kaomoji_literal"), true); - assert.equal(hasNodeType(closed, "inline_code"), true); - assert.equal(codeMarkdown.stream?.stats?.().lastMode, "tail"); - } -}); - -test("compact rule protects a broad syntax-sensitive open-corpus subset", () => { - const values = [...new Set(Object.values(kaomojiCollection).flat())]; - const stock = getMarkdown("kaomoji-corpus-stock"); - const guarded = configureKaomojiMarkdown(getMarkdown("kaomoji-corpus-guarded")); - let vulnerable = 0; - let protectedCount = 0; - - for (const value of values) { - if (!hasFormattingNode(parseMarkdownToStructure(value, stock, { final: true }))) continue; - vulnerable += 1; - if (!hasFormattingNode(parseMarkdownToStructure(value, guarded, { final: true }))) protectedCount += 1; - } - - assert.ok(vulnerable > 8_000, `expected a broad syntax-sensitive corpus, got ${vulnerable}`); - assert.ok(protectedCount >= 7_000, `${protectedCount}/${vulnerable} faces stayed literal`); - assert.ok(protectedCount / vulnerable >= 0.84, `${protectedCount}/${vulnerable} faces stayed literal`); -}); - -function hasFormattingNode(nodes) { - return [...walkNodes(nodes)].some((node) => FORMATTING_NODES.has(node.type)); -} - -function hasNodeType(nodes, type) { - return [...walkNodes(nodes)].some((node) => node.type === type); -} - -function* walkNodes(nodes) { - for (const node of nodes) { - yield node; - if (Array.isArray(node.children)) yield* walkNodes(node.children); - } -} diff --git a/frontend/chat/src/message-rendering-policy.test.mjs b/frontend/chat/src/message-rendering-policy.test.mjs deleted file mode 100644 index 2d59ae476..000000000 --- a/frontend/chat/src/message-rendering-policy.test.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - canBatchStreamingMarkdown, - detectMessageRenderingFeatures, - messageNeedsMarkdown, -} from "./message-rendering-policy.ts"; - -test("plain chat does not load rich Markdown engines", () => { - assert.deepEqual(detectMessageRenderingFeatures("普通消息,价格为 $5。"), { - code: false, - math: false, - mermaid: false, - }); - assert.equal(messageNeedsMarkdown("普通消息,价格为 $5。\n第二行仍是普通文本。"), false); -}); - -test("rich Markdown engines are selected independently", () => { - assert.deepEqual( - detectMessageRenderingFeatures("```ts\nconst answer = 42\n```\n\n$$x^2$$"), - { code: true, math: true, mermaid: false }, - ); - assert.deepEqual( - detectMessageRenderingFeatures("```mermaid\ngraph TD\nA-->B\n```"), - { code: false, math: false, mermaid: true }, - ); - assert.deepEqual( - detectMessageRenderingFeatures("~~~python\nprint('tilde fence')\n~~~"), - { code: true, math: false, mermaid: false }, - ); - assert.deepEqual( - detectMessageRenderingFeatures("~~~~mermaid\ngraph TD\nA-->B\n~~~~"), - { code: false, math: false, mermaid: true }, - ); - assert.equal(messageNeedsMarkdown("**重点** 和 [链接](https://example.com)"), true); - assert.equal(messageNeedsMarkdown("- 第一项\n- 第二项"), true); -}); - -test("fenced code detection respects marker type and fence length", () => { - assert.deepEqual( - detectMessageRenderingFeatures("````mermaid\n~~~\n```\n````"), - { code: false, math: false, mermaid: true }, - ); - assert.deepEqual( - detectMessageRenderingFeatures(" ```ts\n const value = 1\n ```"), - { code: false, math: false, mermaid: false }, - ); -}); - -test("streaming Markdown batching only skips small append-only growth", () => { - assert.equal(canBatchStreamingMarkdown("## 标题", "## 标题abc", 4), true); - assert.equal(canBatchStreamingMarkdown("## 标题", "## 标题abcd", 4), false); - assert.equal(canBatchStreamingMarkdown("旧内容", "替换内容", 4), false); - assert.equal(canBatchStreamingMarkdown("## 标题", "## 标题a", 1), false); -}); diff --git a/frontend/chat/src/mobile-bridge.test.mjs b/frontend/chat/src/mobile-bridge.test.mjs deleted file mode 100644 index 6d87cd54c..000000000 --- a/frontend/chat/src/mobile-bridge.test.mjs +++ /dev/null @@ -1,53 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { installMobileBridge } from "./mobile-bridge.ts"; - -const EXPECTED_METHODS = [ - "requestSnapshot", "selectSession", "removeUnavailableSession", "createSession", - "restartPairing", "reloadFromServer", "exportDiagnostics", "openSettings", - "chooseAttachments", "removeAttachment", "retryAttachment", "continueMeteredTransfer", - "retryFailedMessage", "saveReadingPosition", "markSessionReadThrough", "navigationTargetHandled", - "retryDownloadedAttachment", "touchDownloadedAttachment", "openDownloadedAttachment", - "shareDownloadedAttachment", "saveDownloadedAttachment", "setWebHistoryActive", "dismissError", - "shareText", "saveComposerDraft", "commitSharedText", "rejectSharedText", "sendMessage", - "copyText", "performActionHaptic", "sendCommand", "refreshRuntimeInspection", - "openRuntimeDocument", "openRuntimeMcp", "openRuntimeJob", "clearRuntimeInspectionDetail", - "stopTurn", "queryPluginUi", "cancelPluginUiOwner", "setTheme", "setModelSelection", "reportHealthy", -]; - -function installFor(url) { - const messages = []; - globalThis.window = { - location: { href: url }, - AkashicNativeTransport: { postMessage: (message) => messages.push(JSON.parse(message)) }, - }; - installMobileBridge(); - return { bridge: window.AkashicNative, messages }; -} - -test("embedded and remote WebUI install one generation-bound native surface", () => { - const remote = installFor("https://mobile.invalid/mobile.html?generation_id=remote-gen&nonce=remote-nonce"); - const embedded = installFor("file:///android_asset/mobile.html?generation_id=embedded&nonce=baseline"); - assert.deepEqual(Object.keys(remote.bridge).sort(), [...EXPECTED_METHODS].sort()); - assert.deepEqual(Object.keys(embedded.bridge).sort(), [...EXPECTED_METHODS].sort()); - - assert.throws(() => remote.bridge.selectSession(), /expects 1 args/); - remote.bridge.requestSnapshot(); - embedded.bridge.reportHealthy(); - assert.deepEqual(remote.messages[0], { - v: 1, - generation_id: "remote-gen", - nonce: "remote-nonce", - method: "requestSnapshot", - args: [], - }); - assert.deepEqual(embedded.messages[0], { - v: 1, - generation_id: "embedded", - nonce: "baseline", - method: "reportHealthy", - args: [], - }); - delete globalThis.window; -}); diff --git a/frontend/chat/src/mobile-message-state.test.mjs b/frontend/chat/src/mobile-message-state.test.mjs index 1083b3e31..875c7bc6d 100644 --- a/frontend/chat/src/mobile-message-state.test.mjs +++ b/frontend/chat/src/mobile-message-state.test.mjs @@ -315,17 +315,6 @@ test("reply navigation only resolves a target from the current message projectio assert.equal(resolveMobileReplyNavigationTarget("old-history", [user, assistant]), null); }); -test("reply navigation announces user and assistant identity with message time", () => { - assert.equal( - formatMobileReplyNavigationAnnouncement(selectableMessage("question", "user", "问题"), () => "10:21"), - "已跳到你 10:21 的消息", - ); - assert.equal( - formatMobileReplyNavigationAnnouncement(selectableMessage("answer", "assistant", "回答"), () => "10:22"), - "已跳到Akashic 10:22 的消息", - ); -}); - test("composer waits until every attachment is ready", () => { assert.equal(allMobileAttachmentsReady([]), true); assert.equal(allMobileAttachmentsReady([{ state: "ready" }, { state: "ready" }]), true); diff --git a/frontend/chat/src/mobile-plugin-layout.test.mjs b/frontend/chat/src/mobile-plugin-layout.test.mjs deleted file mode 100644 index a6a0bfd3c..000000000 --- a/frontend/chat/src/mobile-plugin-layout.test.mjs +++ /dev/null @@ -1,385 +0,0 @@ -import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; -import test from "node:test"; - -const platformStyles = await readFile( - new URL("./mobile-native.css", import.meta.url), - "utf8", -); -const sharedStyles = await readFile( - new URL("./message-view.css", import.meta.url), - "utf8", -); -const themeStyles = await readFile( - new URL("./theme.css", import.meta.url), - "utf8", -); -const desktopStyles = await readFile( - new URL("./styles.css", import.meta.url), - "utf8", -); -const dashboardStyles = await readFile( - new URL("../../dashboard/src/styles.css", import.meta.url), - "utf8", -); -const desktopSource = await readFile( - new URL("./desktop-chat-view.tsx", import.meta.url), - "utf8", -); -const desktopAppSource = await readFile( - new URL("./desktop-chat-app.tsx", import.meta.url), - "utf8", -); -const desktopControllerSource = await readFile( - new URL("./use-desktop-chat-controller.ts", import.meta.url), - "utf8", -); -const desktopConversationSource = await readFile( - new URL("./desktop-conversation.tsx", import.meta.url), - "utf8", -); -const desktopSidebarSource = await readFile( - new URL("./desktop-sidebar.tsx", import.meta.url), - "utf8", -); -const mobileSource = await readFile( - new URL("./mobile-native.tsx", import.meta.url), - "utf8", -); -const pluginRuntimeSource = await readFile( - new URL("./mobile-plugin-runtime.tsx", import.meta.url), - "utf8", -); -const sharedMessageSource = await readFile( - new URL("./message-view.tsx", import.meta.url), - "utf8", -); -const messageResponseSource = await readFile( - new URL("./components/ai-elements/message-response.tsx", import.meta.url), - "utf8", -); -const katexStylesSource = await readFile( - new URL("./katex-styles.ts", import.meta.url), - "utf8", -); -const navigationSource = await readFile( - new URL("./conversation-navigation.tsx", import.meta.url), - "utf8", -); -const navigationStyles = await readFile( - new URL("./conversation-navigation.css", import.meta.url), - "utf8", -); -const runtimeDashboardSource = await readFile( - new URL("./runtime-dashboard.tsx", import.meta.url), - "utf8", -); -const runtimeDashboardStyles = await readFile( - new URL("./runtime-dashboard.css", import.meta.url), - "utf8", -); - -test("pending plugin projections never enter the immutable result cache", () => { - assert.match( - pluginRuntimeSource, - /request\.cacheKey[\s\S]*?result\.pending !== true[\s\S]*?immutableResults\.set/, - ); -}); - -test("process plugin slots align with thinking and tool content", () => { - assert.match( - sharedStyles, - /\.process-item\s*\{[\s\S]*?grid-template-columns:\s*var\(--process-rail-width\) minmax\(0, 1fr\);[\s\S]*?column-gap:\s*10px;/, - ); - assert.match( - sharedStyles, - /\.mobile-plugin-slot\[data-slot="turn\.before_reasoning"\],[\s\S]*?margin-inline-start:\s*30px;/, - ); - assert.doesNotMatch(platformStyles, /\.mobile-plugin-slot\[data-slot="turn\.before_reasoning"\]/); - assert.match( - sharedStyles, - /\.process-line\s*\{[\s\S]*?bottom:\s*0;[\s\S]*?left:\s*0;[\s\S]*?width:\s*var\(--process-rail-width\);/, - ); - assert.match( - sharedStyles, - /\.process-line::before\s*\{[^}]*top:\s*0;[^}]*bottom:\s*0;[^}]*width:\s*1px;/, - ); - assert.doesNotMatch(sharedMessageSource, /ResizeObserver/); - assert.doesNotMatch(sharedStyles, /transition:\s*height/); - assert.match( - sharedStyles, - /\.process-node\.diamond\s*\{[^}]*width:\s*6px;[^}]*height:\s*6px;/, - ); - assert.match(sharedStyles, /\.process-panel\s*\{/); - assert.match(sharedStyles, /\.process-panel-body\s*\{/); - assert.match(sharedStyles, /\.process-collapse\s*\{/); - assert.doesNotMatch(sharedStyles, /max-height:\s*min\(52vh/); -}); - -test("streaming thinking uses the shared Markstream renderer", () => { - assert.match( - sharedMessageSource, - /function ThinkingStep[\s\S]*?\{block\.content\}<\/LazyMessageResponse>/, - ); - assert.match( - sharedStyles, - /\.process-markdown\s*\{[^}]*white-space:\s*normal;/, - ); - assert.match( - sharedStyles, - /\.process-markdown-fallback\s*\{[^}]*white-space:\s*pre-wrap;/, - ); -}); - -test("mobile keeps plain streams lightweight and sends Markdown through Markstream", () => { - const processStart = mobileSource.indexOf("const MobileStreamingProcessTrace"); - const processEnd = mobileSource.indexOf("const MobileStreamingToolStep", processStart); - assert.ok(processStart >= 0 && processEnd > processStart); - const streamingProcessSource = mobileSource.slice(processStart, processEnd); - assert.match( - mobileSource, - /source\.streaming && source\.role === "assistant"[\s\S]*? { - assert.match(messageResponseSource, /import\("@\/katex-styles"\)/); - assert.doesNotMatch(messageResponseSource, /^import "katex\/dist\/katex\.min\.css";/m); - assert.match(katexStylesSource, /^import "katex\/dist\/katex\.min\.css";/m); -}); - -test("mobile virtualizer checks its measured tail without forcing a DOM scroll extent read", () => { - const onChangeStart = mobileSource.indexOf("onChange(instance)"); - const onChangeEnd = mobileSource.indexOf("const jumpToMessage", onChangeStart); - assert.ok(onChangeStart >= 0 && onChangeEnd > onChangeStart); - const onChangeSource = mobileSource.slice(onChangeStart, onChangeEnd); - assert.match(onChangeSource, /instance\.getTotalSize\(\) - \(instance\.scrollRect\?\.height \?\? 0\)/); - assert.doesNotMatch(onChangeSource, /instance\.isAtEnd\(/); -}); - -test("desktop shares plugin shell slots without exposing mobile dashboards", () => { - assert.match(desktopControllerSource, /import \{ loadWebPluginCatalog \} from "\.\/mobile-plugin-runtime";/); - assert.match(desktopConversationSource, /import \{ MobilePluginSlot \} from "\.\/mobile-plugin-runtime";/); - assert.match(desktopConversationSource, /name="turn\.before_reasoning"/); - assert.match(desktopConversationSource, /name="turn\.before_tool"/); - assert.match(desktopConversationSource, /name="turn\.after_answer"/); - assert.doesNotMatch(desktopControllerSource, /MobilePluginDashboard|useMobilePluginDashboards/); - assert.doesNotMatch(desktopConversationSource, /MobilePluginDashboard|useMobilePluginDashboards/); - assert.match(pluginRuntimeSource, /fetch\("\/api\/chat\/plugin-ui\/catalog"/); - assert.match(pluginRuntimeSource, /fetch\("\/api\/chat\/plugin-ui\/query"/); - assert.match(pluginRuntimeSource, /slot === "dashboard\.main"/); -}); - -test("desktop and mobile keep one shared conversation owner", () => { - assert.match(sharedStyles, /\.tool-step-disclosure\s*\{/); - assert.match(sharedStyles, /\.message-reply-reference\s*\{/); - assert.match(sharedStyles, /\.agent-content ul\s*\{[\s\S]*?list-style:\s*disc;/); - assert.match(sharedStyles, /\.agent-content ol\s*\{[\s\S]*?list-style:\s*decimal;/); - assert.doesNotMatch(platformStyles, /\.tool-step-disclosure\s*\{/); - assert.doesNotMatch(platformStyles, /\.agent-content (?:ul|ol)\s*\{/); - assert.doesNotMatch(desktopStyles, /\.tool-step-disclosure\s*\{/); - assert.match(desktopAppSource, /import "\.\/message-view\.css";/); - assert.match(mobileSource, /import "\.\/message-view\.css";/); - assert.match(desktopSidebarSource, / { - assert.doesNotMatch(navigationSource, /对话与知识/); - assert.doesNotMatch(navigationSource, />Akashic]*">会话/); - assert.match(navigationSource, /featuredDestinations/); - assert.match(mobileSource, /label: "知识与运行",[\s\S]*?featured: true,/); - assert.match( - navigationStyles, - /\.conversation-destination__icon\s*\{[^}]*width:\s*24px;[^}]*background:\s*transparent;/, - ); - assert.match( - navigationStyles, - /\.conversation-navigation__action\.primary\s*\{[^}]*width:\s*fit-content;[^}]*border-radius:\s*var\(--md-sys-shape-corner-full\);/, - ); - assert.match( - navigationStyles, - /\.conversation-session-list\s*\{[^}]*min-height:\s*0;[^}]*flex:\s*1;[^}]*grid-auto-rows:\s*min-content;[^}]*align-content:\s*start;[^}]*overflow-y:\s*auto;/, - ); - assert.match( - navigationSource, - /
[\s\S]*?<\/section>[\s\S]*?conversation-navigation__auxiliary/, - ); - assert.match( - navigationStyles, - /\.conversation-navigation__auxiliary\s*\{[^}]*position:\s*relative;[^}]*height:\s*80px;[^}]*flex:\s*0 0 80px;/, - ); - assert.match( - sharedStyles, - /\.mobile-plugin-slot\[data-slot="drawer\.panel"\]\s*\{[^}]*position:\s*absolute;[^}]*inset-block-end:\s*0;[^}]*background:\s*var\(--ak-color-bg-canvas\);/, - ); - assert.match( - navigationStyles, - /\.conversation-destination\.featured\s*\{[^}]*min-height:\s*68px;[^}]*border-radius:\s*22px;[^}]*background:\s*var\(--ak-color-action-primary\);[^}]*box-shadow:\s*none;/, - ); - assert.match( - platformStyles, - /\.conversation-navigation\.mobile-drawer\s*\{[^}]*box-shadow:\s*4px 0 12px rgb\(var\(--md-sys-color-shadow-rgb\) \/ 0\.16\);/, - ); - assert.match( - platformStyles, - /\.conversation-navigation\.mobile-drawer\s*\{[^}]*width:\s*min\(84vw, 360px\);/, - ); - assert.match( - platformStyles, - /\.mobile-drawer-scrim\s*\{[^}]*background:\s*rgb\(var\(--ak-color-shadow-rgb\) \/ 0\.54\);/, - ); -}); - -test("composer growth uses the expanded card radius and keeps mobile text centered", () => { - assert.match( - desktopStyles, - /\.composer\.is-expanded\s*\{[^}]*height:\s*auto;[^}]*border-radius:\s*var\(--composer-expanded-radius\);/, - ); - assert.match( - desktopStyles, - /\.composer\s*\{[^}]*overflow:\s*hidden;/, - ); - assert.match( - platformStyles, - /\.mobile-composer textarea\s*\{[^}]*min-height:\s*44px;[^}]*padding:\s*10px 6px 12px;/, - ); -}); - -test("Material shadow roles always declare opacity at use sites", () => { - for (const styles of [themeStyles, platformStyles, desktopStyles, navigationStyles, dashboardStyles]) { - assert.doesNotMatch(styles, /var\(--ak-color-shadow\)/); - } -}); - -test("runtime metrics keep values and categories on one compact baseline", () => { - assert.match( - runtimeDashboardSource, - /
\{label\}<\/small>\{value\}<\/strong><\/div>/, - ); - assert.match( - runtimeDashboardStyles, - /\.runtime-metric div\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*baseline;/, - ); - assert.match( - runtimeDashboardStyles, - /\.runtime-metric strong\s*\{[^}]*order:\s*-1;/, - ); -}); - -test("mobile attachment previews preserve intrinsic aspect ratios", () => { - assert.match( - platformStyles, - /\.message-attachment-preview\s*\{[^}]*display:\s*grid;[^}]*min-block-size:\s*44px;[^}]*place-items:\s*center;/, - ); - assert.match( - platformStyles, - /\.message-attachment-preview img\s*\{[^}]*width:\s*auto;[^}]*max-width:\s*100%;[^}]*height:\s*auto;[^}]*max-height:\s*260px;[^}]*object-fit:\s*contain;/, - ); - assert.doesNotMatch( - platformStyles, - /\.message-attachment-preview img\s*\{[^}]*(?:max-block-size|min\(40vh)/, - ); -}); - -test("mobile scroll control is anchored outside the virtual scroll plane", () => { - assert.match( - mobileSource, - /
[\s\S]*?
\s* { - assert.match( - platformStyles, - /\.mobile-role-divider\s*\{[^}]*height:\s*1px;[^}]*margin-block:\s*-14px 13px;/, - ); - assert.doesNotMatch(platformStyles, /\.mobile-role-divider\s*\{[^}]*margin-block:\s*-7px;/); -}); - -test("virtual search highlight waits for its target row to mount", () => { - assert.match( - mobileSource, - /const register = \(\) => \{[\s\S]*attempts < 4[\s\S]*requestAnimationFrame\(register\)/, - ); -}); - -test("dynamic message measurement stays in the ResizeObserver frame", () => { - assert.doesNotMatch(mobileSource, /useAnimationFrameWithResizeObserver:\s*true/); -}); - -test("full native snapshots commit without waiting for another animation frame", () => { - const receiver = mobileSource.match(/receiveSnapshot\(next\) \{[\s\S]*?\n[ ]{6}\},\n[ ]{6}receiveStreamPatch/); - assert.ok(receiver, "mobile snapshot receiver must remain discoverable"); - assert.match(receiver[0], /nextSnapshot = parseMobileSnapshot\(next\)/); - assert.match(receiver[0], /setSnapshot\(nextSnapshot\)/); - assert.doesNotMatch(receiver[0], /requestAnimationFrame|startTransition/); -}); - -test("stream patches publish by frame while terminal remains immediate", () => { - const receiver = mobileSource.match(/receiveStreamPatch\(next\) \{[\s\S]*?\n[ ]{6}\},\n[ ]{6}receiveStatePatch/); - assert.ok(receiver, "mobile stream receiver must remain discoverable"); - assert.match(receiver[0], /streamSnapshotRef\.current = nextSnapshot/); - assert.match(receiver[0], /nextMessage\.streaming\) streamStore\.publishFrame\(/); - assert.match(receiver[0], /else streamStore\.publishImmediate\(/); - assert.doesNotMatch(receiver[0], /requestAnimationFrame/); - assert.doesNotMatch(receiver[0], /startTransition/); -}); - -test("streaming redraws only dynamic message subtrees", () => { - assert.match(mobileSource, /useSyncExternalStore\(subscribe, getSnapshot, getSnapshot\)/); - assert.match(mobileSource, /const MessageMeta = React\.memo/); - assert.match(mobileSource, /blocks: toCachedAgentBlocks\(message\.blocks\)/); - assert.match(sharedMessageSource, /const MessageBody = memo/); - assert.match(sharedMessageSource, /const MessageAttachments = memo/); - assert.match(sharedMessageSource, /const ProcessTrace = memo/); -}); - -test("user message bubble uses a defined secondary container token", () => { - assert.match(platformStyles, /\.mobile-plain-message-view\.user[\s\S]*?background:\s*var\(--ak-color-action-soft\)/); - assert.doesNotMatch(themeStyles, /--m-secondary-container:/); -}); - -test("fixed mobile chrome stays opaque over the native window", () => { - assert.match( - platformStyles, - /\.mobile-topbar\s*\{[^}]*background:\s*var\(--md-sys-color-surface\);/, - ); - assert.match( - platformStyles, - /\.mobile-composer-zone\s*\{[^}]*background:\s*var\(--md-sys-color-surface\);/, - ); - assert.doesNotMatch( - platformStyles, - /\.(?:mobile-topbar|mobile-composer-zone)\s*\{[^}]*background:\s*color-mix\([^}]*transparent/, - ); -}); - -test("cached images retry once and degrade to an openable file instead of a blank card", () => { - assert.match(mobileSource, /\^image\\\/\/i\.test\(attachment\.contentType\.trim\(\)\)/); - assert.match(mobileSource, /if \(imageRetry === 0\) setImageRetry\(1\);[\s\S]*else setImageUnavailable\(true\);/); - assert.match(mobileSource, /imageUrl && !imageUnavailable/); -}); diff --git a/frontend/chat/src/mobile-plugin-query-queue.test.mjs b/frontend/chat/src/mobile-plugin-query-queue.test.mjs deleted file mode 100644 index 87541c7e0..000000000 --- a/frontend/chat/src/mobile-plugin-query-queue.test.mjs +++ /dev/null @@ -1,109 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - MOBILE_PLUGIN_QUERY_MAX_PENDING, - MobilePluginQueryQueue, -} from "./mobile-plugin-query-queue.ts"; - -function request(ownerId, interactive = true) { - return { ownerId, interactive, started: false }; -} - -function drain(queue, sent) { - while (true) { - const next = queue.startNext(); - if (!next) return; - sent.push(next[0]); - } -} - -test("synchronous query bursts cap total pending before the native bridge", () => { - const queue = new MobilePluginQueryQueue((item) => item.interactive); - const sent = []; - for (let index = 0; index < MOBILE_PLUGIN_QUERY_MAX_PENDING; index += 1) { - queue.enqueue(`request-${index}`, request("owner-a")); - drain(queue, sent); - } - - assert.equal(queue.pendingCount, 128); - assert.equal(queue.activeCount, 4); - assert.equal(queue.queuedCount, 124); - assert.deepEqual(sent, ["request-0", "request-1", "request-2", "request-3"]); - assert.throws( - () => queue.enqueue("request-overflow", request("owner-a")), - /插件未完成请求已达上限(最多 128 个)/, - ); - assert.equal(queue.pendingCount, 128); - assert.equal(queue.queuedCount, 124); -}); - -test("a reply releases one pending slot and starts the next queued request", () => { - const queue = new MobilePluginQueryQueue((item) => item.interactive); - const sent = []; - for (let index = 0; index < MOBILE_PLUGIN_QUERY_MAX_PENDING; index += 1) { - queue.enqueue(`request-${index}`, request("owner-a")); - drain(queue, sent); - } - - assert.equal(queue.complete("request-0")?.ownerId, "owner-a"); - drain(queue, sent); - - assert.equal(queue.pendingCount, 127); - assert.equal(queue.activeCount, 4); - assert.equal(queue.queuedCount, 123); - assert.equal(sent.at(-1), "request-4"); - queue.enqueue("request-replacement", request("owner-b")); - drain(queue, sent); - assert.equal(queue.pendingCount, 128); - assert.equal(queue.queuedCount, 124); -}); - -test("owner cancellation releases both active and queued capacity", () => { - const queue = new MobilePluginQueryQueue((item) => item.interactive, 6, 2, 1); - const sent = []; - ["a-0", "a-1", "a-2"].forEach((requestId) => { - queue.enqueue(requestId, request("owner-a")); - drain(queue, sent); - }); - ["b-0", "b-1", "b-2"].forEach((requestId) => { - queue.enqueue(requestId, request("owner-b")); - drain(queue, sent); - }); - assert.equal(queue.pendingCount, 6); - assert.equal(queue.activeCount, 2); - assert.equal(queue.queuedCount, 4); - - const removed = queue.removeOwner("owner-a"); - assert.deepEqual(removed.map(([requestId]) => requestId), ["a-0", "a-1", "a-2"]); - assert.deepEqual(removed.map(([, item]) => item.started), [true, true, false]); - drain(queue, sent); - - assert.equal(queue.pendingCount, 3); - assert.equal(queue.activeCount, 2); - assert.equal(queue.queuedCount, 1); - ["c-0", "c-1", "c-2"].forEach((requestId) => { - queue.enqueue(requestId, request("owner-c")); - drain(queue, sent); - }); - assert.equal(queue.pendingCount, 6); - assert.throws( - () => queue.enqueue("still-full", request("owner-c")), - /未完成请求已达上限(最多 6 个)/, - ); -}); - -test("invalid scheduler budgets and duplicate identities fail loudly", () => { - const queue = new MobilePluginQueryQueue((item) => item.interactive, 2, 1, 1); - queue.enqueue("same", request("owner")); - - assert.throws(() => queue.enqueue("same", request("owner")), /身份重复或为空/); - assert.throws( - () => new MobilePluginQueryQueue((item) => item.interactive, 0, 1, 1), - /总量预算无效/, - ); - assert.throws( - () => new MobilePluginQueryQueue((item) => item.interactive, 1, 2, 1), - /并发预算无效/, - ); -}); diff --git a/frontend/chat/src/mobile-plugin-result-cache.test.mjs b/frontend/chat/src/mobile-plugin-result-cache.test.mjs deleted file mode 100644 index ab6358b93..000000000 --- a/frontend/chat/src/mobile-plugin-result-cache.test.mjs +++ /dev/null @@ -1,72 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { MobilePluginResultCache } from "./mobile-plugin-result-cache.ts"; - -test("entry quota evicts the least recently used immutable result", () => { - const cache = new MobilePluginResultCache(2, 100); - cache.set("first", '{"value":1}'); - cache.set("second", '{"value":2}'); - - assert.equal(cache.get("first"), '{"value":1}'); - cache.set("third", '{"value":3}'); - - assert.equal(cache.get("second"), undefined); - assert.equal(cache.get("first"), '{"value":1}'); - assert.equal(cache.get("third"), '{"value":3}'); - assert.equal(cache.size, 2); - assert.equal(cache.byteSize, 22); -}); - -test("byte quota evicts oldest results until the aggregate fits", () => { - const cache = new MobilePluginResultCache(10, 10); - cache.set("first", "1111"); - cache.set("second", "2222"); - cache.set("third", "3333"); - - assert.equal(cache.get("first"), undefined); - assert.equal(cache.get("second"), "2222"); - assert.equal(cache.get("third"), "3333"); - assert.equal(cache.byteSize, 8); -}); - -test("oversized result fails loudly without evicting unrelated entries", () => { - const cache = new MobilePluginResultCache(10, 10); - cache.set("kept", "safe"); - - assert.throws( - () => cache.set("oversized", "12345678901"), - /超过总字节预算/, - ); - assert.equal(cache.get("oversized"), undefined); - assert.equal(cache.get("kept"), "safe"); - assert.equal(cache.byteSize, 4); -}); - -test("clear drops all catalog-scoped results and resets byte accounting", () => { - const cache = new MobilePluginResultCache(10, 10); - cache.set("first", "data"); - - cache.clear(); - - assert.equal(cache.get("first"), undefined); - assert.equal(cache.size, 0); - assert.equal(cache.byteSize, 0); -}); - -test("invalid cache invariants fail loudly", () => { - const cache = new MobilePluginResultCache(); - - assert.throws(() => cache.set("", "invalid"), /身份为空/); - assert.throws(() => new MobilePluginResultCache(0, 1), /条数预算无效/); - assert.throws(() => new MobilePluginResultCache(1, 0), /字节预算无效/); -}); - -test("byte quota measures UTF-8 content rather than JavaScript character count", () => { - const cache = new MobilePluginResultCache(10, 4); - - cache.set("chinese", "中"); - cache.set("ascii", "a"); - - assert.equal(cache.byteSize, 4); -}); diff --git a/frontend/chat/src/mobile-surface-history.test.mjs b/frontend/chat/src/mobile-surface-history.test.mjs deleted file mode 100644 index 7526e07ae..000000000 --- a/frontend/chat/src/mobile-surface-history.test.mjs +++ /dev/null @@ -1,77 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - pushMobileSurface, - readMobileSurfaceHistoryState, - replaceMobileSurface, -} from "./mobile-surface-history.ts"; - -class FakeHistory { - entries = []; - index = -1; - - replaceState(data) { - if (this.index < 0) { - this.entries.push(data); - this.index = 0; - } else { - this.entries[this.index] = data; - } - } - - pushState(data) { - this.entries.splice(this.index + 1); - this.entries.push(data); - this.index += 1; - } - - back() { - this.index -= 1; - return this.entries[this.index]; - } -} - -test("dashboard back stack returns to directory then chat without losing plugin identity", () => { - const history = new FakeHistory(); - replaceMobileSurface(history, { kind: "chat" }); - pushMobileSurface(history, { kind: "plugins" }); - pushMobileSurface(history, { kind: "dashboard", pluginId: "status_commands" }); - - assert.deepEqual(readMobileSurfaceHistoryState(history.entries[history.index]), { - kind: "dashboard", - pluginId: "status_commands", - }); - assert.deepEqual(readMobileSurfaceHistoryState(history.back()), { kind: "plugins" }); - assert.deepEqual(readMobileSurfaceHistoryState(history.back()), { kind: "chat" }); -}); - -test("runtime detail returns to expanded runtime directory then chat", () => { - const history = new FakeHistory(); - replaceMobileSurface(history, { kind: "chat" }); - pushMobileSurface(history, { kind: "runtime" }); - pushMobileSurface(history, { - kind: "runtime-detail", - detailKind: "document", - key: "memory", - }); - - assert.deepEqual(readMobileSurfaceHistoryState(history.entries[history.index]), { - kind: "runtime-detail", - detailKind: "document", - key: "memory", - }); - assert.deepEqual(readMobileSurfaceHistoryState(history.back()), { kind: "runtime" }); - assert.deepEqual(readMobileSurfaceHistoryState(history.back()), { kind: "chat" }); -}); - -test("foreign or malformed history state returns to chat", () => { - assert.deepEqual(readMobileSurfaceHistoryState(null), { kind: "chat" }); - assert.deepEqual( - readMobileSurfaceHistoryState({ - akashicMobileSurface: true, - surface: { kind: "dashboard", pluginId: "" }, - }), - { kind: "chat" }, - ); -}); diff --git a/frontend/chat/src/mobile-turn-trace.test.mjs b/frontend/chat/src/mobile-turn-trace.test.mjs deleted file mode 100644 index ffe84e185..000000000 --- a/frontend/chat/src/mobile-turn-trace.test.mjs +++ /dev/null @@ -1,337 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - MOBILE_TURN_MISSING, - MOBILE_TURN_TRACE_MAX_TRACKED, - MobileTurnTraceRegistry, - mobileTurnFirstVisibleKinds, - mobileTurnTraceEmit, - parseMobileTurnId, -} from "./mobile-turn-trace.ts"; - -function captureSink() { - const records = []; - return { - records, - sink(record) { - records.push(record); - }, - }; -} - -test("turn ids parse only from the non-empty assistant message contract", () => { - assert.equal(parseMobileTurnId("assistant:turn-01J"), "turn-01J"); - assert.equal(parseMobileTurnId("assistant:"), undefined); - assert.equal(parseMobileTurnId("user:turn-01J"), undefined); - assert.equal(parseMobileTurnId(""), undefined); -}); - -test("full identity is session + turn + client_message_id", () => { - const registry = new MobileTurnTraceRegistry(() => {}); - const identity = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - assert.equal(identity.sessionId, "session-1"); - assert.equal(identity.turnId, "turn-1"); - assert.equal(identity.clientMessageId, "client-1"); - assert.equal(registry.identityFor("session-1", "turn-1").clientMessageId, "client-1"); -}); - -test("missing identity parts are marked explicitly, never guessed", () => { - const registry = new MobileTurnTraceRegistry(() => {}); - const identity = registry.registerTurnIdentity("session-1", undefined, undefined); - assert.equal(identity.turnId, MOBILE_TURN_MISSING); - assert.equal(identity.clientMessageId, MOBILE_TURN_MISSING); - const noClientId = registry.registerTurnIdentity("session-1", "turn-1", undefined); - assert.equal(noClientId.clientMessageId, MOBILE_TURN_MISSING); -}); - -test("missing client_message_id fills later; conflicting non-missing values degrade to one diagnostic", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - const filled = registry.registerTurnIdentity("session-1", "turn-1", undefined); - assert.equal(filled.clientMessageId, MOBILE_TURN_MISSING); - const later = registry.registerTurnIdentity("session-1", "turn-1", "client-3"); - assert.equal(later.clientMessageId, "client-3"); - assert.equal(registry.identityFor("session-1", "turn-1").clientMessageId, "client-3"); - const conflicting = registry.registerTurnIdentity("session-1", "turn-1", "client-4"); - assert.equal(conflicting.clientMessageId, "client-3"); - assert.equal(captured.records.length, 1); - assert.equal(captured.records[0].event, "webui.identity_conflict"); - assert.equal(captured.records[0].client_message_id, "client-3"); - assert.equal(captured.records[0].incoming_client_message_id, "client-4"); - const again = registry.registerTurnIdentity("session-1", "turn-1", "client-4"); - assert.equal(again.clientMessageId, "client-3"); - assert.equal(captured.records.length, 1); -}); - -test("conflicting non-missing client_message_id keeps the first identity, never throws, markFirst still works", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - const identity = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - assert.doesNotThrow(() => { - const conflicting = registry.registerTurnIdentity("session-1", "turn-1", "client-2"); - assert.equal(conflicting.key, identity.key); - assert.equal(conflicting.clientMessageId, "client-1"); - }); - assert.equal(captured.records.length, 1); - const diagnostic = captured.records[0]; - assert.equal(diagnostic.event, "webui.identity_conflict"); - assert.equal(diagnostic.session_id, "session-1"); - assert.equal(diagnostic.turn_id, "turn-1"); - assert.equal(diagnostic.client_message_id, "client-1"); - assert.equal(diagnostic.incoming_client_message_id, "client-2"); - // 同一 turn+incoming 组合只降级一次;原身份不变 - registry.registerTurnIdentity("session-1", "turn-1", "client-2"); - assert.equal(captured.records.length, 1); - assert.equal(registry.registerTurnIdentity("session-1", "turn-1", "client-1").key, identity.key); - // 随后 markFirst 正常:原身份仍可标记里程碑 - assert.equal(registry.markFirst(identity, "webui.patch_received", "thinking", "origin"), true); - assert.equal(captured.records.length, 2); - assert.equal(captured.records[1].event, "webui.patch_received"); - assert.equal(captured.records[1].client_message_id, "client-1"); -}); - -test("first visible kinds: thinking precedes answer, same patch may introduce both, terminal last", () => { - const empty = { content: "", thinking: [] }; - assert.deepEqual( - mobileTurnFirstVisibleKinds(undefined, { - message: { content: "回答", thinking: ["思考"], streaming: true }, - }), - ["thinking", "answer"], - ); - assert.deepEqual( - mobileTurnFirstVisibleKinds(empty, { - message: { content: "回答", thinking: [], streaming: false }, - }), - ["answer", "terminal"], - ); - assert.deepEqual( - mobileTurnFirstVisibleKinds( - { content: "回答", thinking: ["思考"] }, - { contentAppend: "续写", thinkingAppend: { blockIndex: 0, delta: "续" } }, - ), - [], - ); - assert.deepEqual( - mobileTurnFirstVisibleKinds(empty, { - contentAppend: "回", - thinkingAppend: { blockIndex: 0, delta: "思" }, - }), - ["thinking", "answer"], - ); -}); - -test("identity registration fills missing data and reports each conflict once", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - - const missing = registry.registerTurnIdentity("session-1", "turn-1", undefined); - assert.equal(missing.clientMessageId, MOBILE_TURN_MISSING); - - const filled = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - assert.equal(filled.key, missing.key); - assert.equal(filled.clientMessageId, "client-1"); - - const conflicting = registry.registerTurnIdentity("session-1", "turn-1", "client-2"); - registry.registerTurnIdentity("session-1", "turn-1", "client-2"); - - assert.equal(conflicting.clientMessageId, "client-1"); - assert.equal(captured.records.length, 1); - assert.deepEqual( - { - event: captured.records[0].event, - turn: captured.records[0].turn_id, - current: captured.records[0].client_message_id, - incoming: captured.records[0].incoming_client_message_id, - kind: captured.records[0].kind, - }, - { - event: "webui.identity_conflict", - turn: "turn-1", - current: "client-1", - incoming: "client-2", - kind: "identity", - }, - ); -}); - -test("milestones are unique per event and kind and use the entry's current identity", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - const early = registry.registerTurnIdentity("session-1", "turn-1", undefined); - - assert.equal( - registry.markFirst(early, "webui.patch_received", "thinking", "receive-stream-patch"), - true, - ); - registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - assert.equal( - registry.markFirst(early, "webui.react_committed", "thinking", "message-row"), - true, - ); - assert.equal( - registry.markFirst(early, "webui.react_committed", "thinking", "message-row"), - false, - ); - assert.equal( - registry.markFirst(early, "webui.react_committed", "answer", "message-row"), - true, - ); - - const other = registry.registerTurnIdentity("session-1", "turn-2", "client-2"); - assert.equal( - registry.markFirst(other, "webui.react_committed", "thinking", "message-row"), - true, - ); - - assert.deepEqual( - captured.records.map((record) => [record.turn_id, record.client_message_id, record.kind]), - [ - ["turn-1", MOBILE_TURN_MISSING, "thinking"], - ["turn-1", "client-1", "thinking"], - ["turn-1", "client-1", "answer"], - ["turn-2", "client-2", "thinking"], - ], - ); -}); - -test("canonical message ids resolve through aliases without guessing", () => { - const registry = new MobileTurnTraceRegistry(() => {}); - const identity = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - - assert.equal(registry.identityForMessage("session-1", "message:canonical"), undefined); - const bound = registry.bindMessageIdentity("session-1", "message:canonical", identity); - - assert.equal(bound.key, identity.key); - assert.equal( - registry.identityForMessage("session-1", "assistant:turn-1").key, - identity.key, - ); - assert.equal( - registry.identityForMessage("session-1", "message:canonical").key, - identity.key, - ); - assert.equal(registry.identityForMessage("session-1", "message:unbound"), undefined); -}); - -test("a live alias keeps its first owner and reports a competing owner once", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - const first = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - const second = registry.registerTurnIdentity("session-1", "turn-2", "client-2"); - - registry.bindMessageIdentity("session-1", "message:canonical", first); - const refused = registry.bindMessageIdentity("session-1", "message:canonical", second); - registry.bindMessageIdentity("session-1", "message:canonical", second); - - assert.equal(refused.key, first.key); - assert.equal( - registry.identityForMessage("session-1", "message:canonical").key, - first.key, - ); - assert.equal(captured.records.length, 1); - assert.equal(captured.records[0].kind, "alias"); - assert.equal(captured.records[0].turn_id, "turn-1"); - - const secondAlias = registry.bindMessageIdentity( - "session-1", - "message:canonical-2", - second, - ); - assert.equal(secondAlias.key, second.key); -}); - -test("bounded eviction removes aliases and stale sources degrade without blocking", () => { - const captured = captureSink(); - const registry = new MobileTurnTraceRegistry(captured.sink); - const doomed = registry.registerTurnIdentity("session-1", "turn-0", "client-0"); - registry.bindMessageIdentity("session-1", "message:canonical-0", doomed); - - for (let index = 1; index <= MOBILE_TURN_TRACE_MAX_TRACKED; index += 1) { - registry.registerTurnIdentity("session-1", `turn-${index}`, `client-${index}`); - } - - assert.equal(registry.identityFor("session-1", "turn-0"), undefined); - assert.equal(registry.identityForMessage("session-1", "message:canonical-0"), undefined); - assert.equal( - registry.bindMessageIdentity("session-1", "message:stale", doomed), - undefined, - ); - assert.equal(captured.records.length, 1); - assert.equal(captured.records[0].kind, "stale_source"); - - const survivor = registry.identityFor("session-1", `turn-${MOBILE_TURN_TRACE_MAX_TRACKED}`); - for (let index = 0; index < MOBILE_TURN_TRACE_MAX_TRACKED * 2; index += 1) { - registry.bindMessageIdentity("session-1", `message:extra-${index}`, survivor); - } - assert.equal(registry.tracks(survivor.key), true); -}); - -test("a failing observation sink cannot block identity or milestone state", () => { - const diagnostics = []; - const originalConsoleError = console.error; - console.error = (line) => diagnostics.push(String(line)); - try { - const registry = new MobileTurnTraceRegistry(() => { - throw new Error("sensitive failure text"); - }); - const first = registry.registerTurnIdentity("session-1", "turn-1", "client-1"); - const second = registry.registerTurnIdentity("session-1", "turn-2", "client-2"); - - assert.equal( - registry.markFirst(first, "webui.react_committed", "terminal", "message-row"), - true, - ); - registry.bindMessageIdentity("session-1", "message:canonical", first); - assert.equal( - registry.bindMessageIdentity("session-1", "message:canonical", second).key, - first.key, - ); - - assert.ok(diagnostics.length >= 2); - for (const line of diagnostics) { - assert.match(line, /^\[akashic-trace\] \{/); - const payload = JSON.parse(line.slice("[akashic-trace] ".length)); - assert.equal(payload.event, "webui.trace_sink_error"); - assert.equal(payload.error_type, "Error"); - assert.ok(!line.includes("sensitive failure text")); - assert.ok(!line.includes("content")); - } - } finally { - console.error = originalConsoleError; - } -}); - -test("the default sink emits one content-free trace line", () => { - const logs = []; - const originalConsoleLog = console.log; - console.log = (line) => logs.push(String(line)); - try { - mobileTurnTraceEmit({ - event: "webui.patch_received", - session_id: "session-1", - turn_id: "turn-1", - client_message_id: "client-1", - wall_ms: 1234, - performance_ms: 56.78, - kind: "thinking", - origin: "receive-stream-patch", - }); - } finally { - console.log = originalConsoleLog; - } - - assert.equal(logs.length, 1); - assert.match(logs[0], /^\[akashic-trace\] \{/); - const payload = JSON.parse(logs[0].slice("[akashic-trace] ".length)); - assert.deepEqual(Object.keys(payload).sort(), [ - "client_message_id", - "event", - "kind", - "origin", - "performance_ms", - "session_id", - "turn_id", - "wall_ms", - ]); -}); diff --git a/frontend/chat/src/model-capsule-data.test.mjs b/frontend/chat/src/model-capsule-data.test.mjs deleted file mode 100644 index 830615963..000000000 --- a/frontend/chat/src/model-capsule-data.test.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; -import { compatibleEffort, groupModelRuntimes } from "./model-capsule-data.ts"; - -const runtime = (id, sourceName, efforts = ["low", "medium"]) => ({ - id, sourceName, supportedReasoningEfforts: efforts, reasoningEffort: "medium", - provider: "fixture", model: id, sourceId: sourceName, roles: ["default"], -}); - -test("model groups retain stable global indexes without render-time rescans", () => { - const groups = groupModelRuntimes([runtime("a", "one"), runtime("b", "two"), runtime("c", "one")]); - assert.deepEqual(groups.map(([source, items]) => [source, items.map(({ index }) => index)]), [ - ["one", [0, 2]], ["two", [1]], - ]); -}); - -test("model effort selection preserves compatible choice and owns fallback order", () => { - assert.equal(compatibleEffort(runtime("a", "one"), "low"), "low"); - assert.equal(compatibleEffort(runtime("a", "one"), "unsupported"), "medium"); - assert.equal(compatibleEffort(runtime("a", "one", ["high"]), ""), "high"); -}); diff --git a/frontend/chat/src/module-boundaries.test.mjs b/frontend/chat/src/module-boundaries.test.mjs deleted file mode 100644 index 49b7344ec..000000000 --- a/frontend/chat/src/module-boundaries.test.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import { readdirSync, readFileSync } from "node:fs"; -import { dirname, extname, join, relative, resolve } from "node:path"; -import test from "node:test"; -import { fileURLToPath } from "node:url"; - -const sourceRoot = dirname(fileURLToPath(import.meta.url)); -const moduleFiles = readdirSync(sourceRoot, { recursive: true }) - .filter((name) => [".ts", ".tsx"].includes(extname(name))) - .map((name) => resolve(sourceRoot, name)); -const moduleSet = new Set(moduleFiles); - -function resolveLocalImport(importer, specifier) { - if (!specifier.startsWith(".")) return null; - const target = resolve(dirname(importer), specifier); - return [target, `${target}.ts`, `${target}.tsx`, join(target, "index.ts"), join(target, "index.tsx")] - .find((candidate) => moduleSet.has(candidate)) ?? null; -} - -function localDependencies(file) { - const source = readFileSync(file, "utf8"); - const dependencies = new Set(); - const imports = /(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s+)?["']([^"']+)["']|import\s*\(\s*["']([^"']+)["']\s*\)/g; - for (const match of source.matchAll(imports)) { - const dependency = resolveLocalImport(file, match[1] ?? match[2]); - if (dependency) dependencies.add(dependency); - } - return [...dependencies]; -} - -test("chat source has an acyclic local module graph", () => { - const graph = new Map(moduleFiles.map((file) => [file, localDependencies(file)])); - const visiting = new Set(); - const visited = new Set(); - - function visit(file, path) { - if (visiting.has(file)) { - const cycleStart = path.indexOf(file); - const cycle = [...path.slice(cycleStart), file].map((item) => relative(sourceRoot, item)); - assert.fail(`circular dependency: ${cycle.join(" -> ")}`); - } - if (visited.has(file)) return; - visiting.add(file); - for (const dependency of graph.get(file) ?? []) visit(dependency, [...path, file]); - visiting.delete(file); - visited.add(file); - } - - for (const file of moduleFiles) visit(file, []); -}); - -test("entry modules are dependency roots", () => { - const entryModules = new Set([resolve(sourceRoot, "main.tsx"), resolve(sourceRoot, "mobile-entry.tsx")]); - const dependents = moduleFiles.flatMap((file) => - localDependencies(file) - .filter((dependency) => entryModules.has(dependency)) - .map((dependency) => `${relative(sourceRoot, file)} -> ${relative(sourceRoot, dependency)}`)); - assert.deepEqual(dependents, []); -}); diff --git a/frontend/chat/src/runtime-dashboard-data.test.mjs b/frontend/chat/src/runtime-dashboard-data.test.mjs deleted file mode 100644 index 364760ce2..000000000 --- a/frontend/chat/src/runtime-dashboard-data.test.mjs +++ /dev/null @@ -1,23 +0,0 @@ -import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; -import test from "node:test"; - -import { runtimeItems } from "./runtime-dashboard-data.ts"; - -const overview = { - documents: [{ id: "doc", title: "文档", relative_path: "docs/doc.md", group: "core", description: "说明", available: true }], - jobs: [{ id: "job", name: "任务", trigger: "schedule", tier: "routine", fire_at: "2026-08-12T00:00:00Z", timezone: "Asia/Shanghai", enabled: true, run_count: 1 }], - capabilities: { snapshot_id: "one", plugins: [], skills: [], mcp_servers: [{ owner_id: "core", name: "filesystem", tool_count: 4 }] }, -}; - -test("runtime directory projection stays independent from React presentation", () => { - assert.equal(runtimeItems("documents", overview)[0].icon, "documents"); - assert.equal(runtimeItems("mcp", overview)[0].key, "core\u0000filesystem"); - assert.equal(runtimeItems("jobs", overview)[0].status, "启用"); -}); - -test("runtime controller derives selection before detail effects", async () => { - const controller = await readFile(new URL("./use-runtime-dashboard.ts", import.meta.url), "utf8"); - assert.match(controller, /const selectedKey = useMemo/); - assert.doesNotMatch(controller, /const \[selectedKey, setSelectedKey\]/); -}); diff --git a/frontend/chat/src/static-message-response.test.mjs b/frontend/chat/src/static-message-response.test.mjs deleted file mode 100644 index 3929b3760..000000000 --- a/frontend/chat/src/static-message-response.test.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { renderStaticMarkdown } from "./static-markdown.ts"; - -test("settled desktop Markdown preserves GFM and fenced code", () => { - const html = renderStaticMarkdown("## 标题\n\n- 一\n- 二\n\n```ts\nconst value = 1;\n```\n\n| A | B |\n| - | - |\n| 1 | 2 |"); - assert.match(html, /

标题<\/h2>/); - assert.match(html, /
    [\s\S]*?
  • 一<\/li>/); - assert.match(html, /class="static-code-block"/); - assert.match(html, /data-static-code-copy/); - assert.match(html, /const value = 1;/); - assert.match(html, //); -}); - -test("settled desktop Markdown keeps raw HTML and unsafe links inert", () => { - const html = renderStaticMarkdown('\n\n[危险](javascript:alert(1))'); - assert.doesNotMatch(html, /'); - writeFileSync(resolve(directory, "main.js"), "export const main = true;\n"); - writeFileSync(resolve(directory, "main.css"), ".main { color: black; }\n"); - writeFileSync(resolve(directory, "nested/lazy.js"), `export const lazy = "${"deterministic-lazy-chunk-".repeat(20)}";\n`); - - const metrics = collectBuildMetrics(directory, "index.html"); - - assert.equal(metrics.initialJavaScript.fileCount, 1); - assert.equal(metrics.initialStylesheets.fileCount, 1); - assert.equal(metrics.artifacts.javascript.fileCount, 2); - assert.equal(metrics.artifacts.fileCount, 4); - assert.equal(metrics.artifacts.largestJavaScript[0].file, "nested/lazy.js"); - } finally { - rmSync(directory, { recursive: true, force: true }); - } -}); - -test("createBuildBaseline gives an explicit five-percent byte budget", () => { - const target = { - initialJavaScript: { gzipBytes: 10_000 }, - initialStylesheets: { gzipBytes: 2_000 }, - artifacts: { - rawBytes: 100_000, - fileCount: 10, - javascript: { gzipBytes: 30_000 }, - }, - }; - const baseline = createBuildBaseline({ sourceCommit: "abc", toolchain: {}, targets: { desktop: target } }); - const budget = baseline.build.budgets.desktop; - - assert.equal(budget.initialJavaScriptGzipBytes, 11_264); - assert.equal(budget.fileCount, 13); - assert.equal(baseline.browser.status, "unmeasured"); -}); - -test("compareBuildMetrics reports every exceeded budget", () => { - const baseline = { - build: { - budgets: { - desktop: { - initialJavaScriptGzipBytes: 10, - initialStylesheetsGzipBytes: 10, - javascriptGzipBytes: 10, - artifactRawBytes: 10, - fileCount: 1, - }, - }, - }, - }; - const current = { - desktop: { - initialJavaScript: { gzipBytes: 11 }, - initialStylesheets: { gzipBytes: 9 }, - artifacts: { javascript: { gzipBytes: 12 }, rawBytes: 10, fileCount: 2 }, - }, - }; - - const failures = compareBuildMetrics(current, baseline).filter((check) => !check.passed); - - assert.deepEqual(failures.map((check) => check.metric), [ - "initialJavaScript.gzipBytes", - "artifacts.javascript.gzipBytes", - "artifacts.fileCount", - ]); -}); diff --git a/scripts/webui-performance/desktop-fixture-server.test.mjs b/scripts/webui-performance/desktop-fixture-server.test.mjs deleted file mode 100644 index 478b4c5ab..000000000 --- a/scripts/webui-performance/desktop-fixture-server.test.mjs +++ /dev/null @@ -1,123 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { resolve } from "node:path"; -import test from "node:test"; - -import WebSocket from "ws"; - -import { startDesktopFixtureServer } from "./desktop-fixture-server.mjs"; - -test("desktop fixture serves both profiles and a real WebSocket stream", async () => { - const root = mkdtempSync(resolve(tmpdir(), "akashic-desktop-fixture-test-")); - writeFileSync(resolve(root, "index.html"), "fixture"); - const fixture = await startDesktopFixtureServer(root); - try { - const sessions = await fetch(`${fixture.origin}/api/chat/sessions`).then((response) => response.json()); - assert.deepEqual(sessions.items.map((item) => item.first_message_content), [ - "性能基线会话", - "纯文本性能会话", - ]); - const plain = await fetch(`${fixture.origin}/api/chat/sessions/perf-session-plain/messages`).then((response) => response.json()); - assert.equal(plain.items.length, 50); - assert.deepEqual([plain.items[0].seq, plain.items.at(-1).seq], [50, 99]); - assert.equal(plain.before_seq, 50); - assert.equal(plain.has_more, true); - const older = await fetch(`${fixture.origin}/api/chat/sessions/perf-session-plain/messages?page_size=50&before_seq=50`).then((response) => response.json()); - assert.deepEqual([older.items[0].seq, older.items.at(-1).seq], [0, 49]); - assert.equal(older.has_more, false); - assert.equal(plain.items.some((item) => item.tool_chain.length > 0), false); - const runtimeDocuments = await fetch(`${fixture.origin}/api/chat/runtime/documents`).then((response) => response.json()); - assert.deepEqual(runtimeDocuments.items.map((item) => item.id), ["projectneed", "workflow"]); - const runtimeMcp = await fetch(`${fixture.origin}/api/chat/runtime/mcp?owner_id=core&name=filesystem`).then((response) => response.json()); - assert.equal(runtimeMcp.markdown, "## filesystem\n\nMCP 详情夹具。"); - const upload = await fetch(`${fixture.origin}/api/chat/uploads?filename=fixture.txt`, { method: "POST", body: "fixture" }).then((response) => response.json()); - assert.equal(upload.upload_path, "uploads/fixture.txt"); - const pairing = await fetch(`${fixture.origin}/api/chat/mobile-pairing`, { method: "POST" }).then((response) => response.json()); - assert.equal(pairing.protocol_version, 1); - const claim = await fetch(`${fixture.origin}/api/chat/mobile-pairing/${pairing.pairing_id}`).then((response) => response.json()); - assert.equal(claim.confirmation_code, "358864"); - const device = await fetch(`${fixture.origin}/api/chat/mobile-pairing/${pairing.pairing_id}/approve`, { method: "POST" }).then((response) => response.json()); - assert.deepEqual(device, { device_id: "pixel-7", display_name: "Pixel 7" }); - const settings = await fetch(`${fixture.origin}/api/settings/model/catalog`).then((response) => response.json()); - assert.equal(settings.models.length, 48); - const socket = new WebSocket(`ws://127.0.0.1:${fixture.port}/ws`); - await new Promise((resolveOpen, reject) => { - socket.once("open", resolveOpen); - socket.once("error", reject); - }); - const frames = []; - socket.on("message", (data) => frames.push(JSON.parse(String(data)))); - const response = await fetch(`${fixture.origin}/__fixture/stream?count=3&delta=x&interval_ms=0`, { method: "POST" }); - assert.equal(response.status, 200); - await new Promise((resolveFrames) => { - const poll = () => frames.length === 5 ? resolveFrames() : setTimeout(poll, 5); - poll(); - }); - assert.deepEqual(frames.map(({ type }) => type), [ - "turn.started", - "answer.delta", - "answer.delta", - "answer.delta", - "message.final", - ]); - assert.equal(frames.at(-1).content, "xxx"); - socket.close(); - } finally { - await fixture.close(); - rmSync(root, { recursive: true, force: true }); - } -}); - -test("desktop fixture replays one rich turn over a long stored history", async () => { - const root = mkdtempSync(resolve(tmpdir(), "akashic-desktop-replay-test-")); - writeFileSync(resolve(root, "index.html"), "fixture"); - const replayTurn = { - content: "final answer", - stages: [{ - text: "stage text", - reasoning: "thinking", - calls: [{ - callId: "call-1", name: "probe", status: "success", - arguments: { scope: "fixture" }, finalArguments: { scope: "fixture" }, result: "done", - }], - }], - }; - const fixture = await startDesktopFixtureServer(root, { historyCount: 1_975, replayTurn }); - try { - const history = await fetch(`${fixture.origin}/api/chat/sessions/perf-session/messages`).then((response) => response.json()); - assert.equal(history.total, 1_975); - assert.deepEqual([history.items[0].seq, history.items.at(-1).seq], [1_925, 1_974]); - const socket = new WebSocket(`ws://127.0.0.1:${fixture.port}/ws`); - await new Promise((resolveOpen, reject) => { - socket.once("open", resolveOpen); - socket.once("error", reject); - }); - const frames = []; - socket.on("message", (data) => frames.push(JSON.parse(String(data)))); - const response = await fetch(`${fixture.origin}/__fixture/stream?mode=replay&characters_per_second=10000&chunk_characters=1`, { method: "POST" }); - assert.equal(response.status, 200); - const summary = await response.json(); - assert.deepEqual({ stages: summary.stageCount, calls: summary.callCount }, { stages: 1, calls: 1 }); - assert.equal(summary.chunkCharacters, 1); - assert.equal(summary.deltaCount, "thinkingstage textfinal answer".length); - assert.equal( - frames.filter((frame) => frame.delta !== undefined).every((frame) => frame.delta.length === 1), - true, - ); - assert.equal(frames[0].type, "turn.started"); - assert.equal(frames.at(-1).type, "message.final"); - assert.equal(frames.filter(({ type }) => type === "react.thinking.delta").length, "thinking".length); - assert.equal( - frames.filter(({ type }) => type === "answer.delta").length, - "stage textfinal answer".length, - ); - assert.equal(frames.filter(({ type }) => type === "react.tool.started").length, 1); - assert.equal(frames.filter(({ type }) => type === "react.tool.completed").length, 1); - assert.equal(frames.at(-1).content, "final answer"); - socket.close(); - } finally { - await fixture.close(); - rmSync(root, { recursive: true, force: true }); - } -}); diff --git a/scripts/webui-performance/fixtures.test.mjs b/scripts/webui-performance/fixtures.test.mjs deleted file mode 100644 index b4b657313..000000000 --- a/scripts/webui-performance/fixtures.test.mjs +++ /dev/null @@ -1,30 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { - desktopMessages, - mobileSnapshot, - mobileStreamPatch, - mobileTerminalPatch, -} from "./fixtures.mjs"; - -test("desktop fixture keeps the requested history size and rich-message cadence", () => { - const messages = desktopMessages(100).items; - - assert.equal(messages.length, 100); - assert.equal(messages.filter((message) => message.content.includes("```ts")).length, 10); - assert.equal(messages.filter((message) => message.tool_chain.length > 0).length, 10); -}); - -test("mobile fixture and stream patches preserve protocol identity", () => { - const snapshot = mobileSnapshot(300, { streaming: true }); - const delta = mobileStreamPatch(snapshot, 0, "片"); - const terminal = mobileTerminalPatch(snapshot, "片".repeat(600)); - - assert.equal(snapshot.messages.length, 300); - assert.equal(snapshot.messages.at(-1).streaming, true); - assert.equal(delta.messageId, snapshot.messages.at(-1).id); - assert.equal(terminal.message.streaming, false); - assert.equal(terminal.state.protocolVersion, 1); - assert.equal("messages" in terminal.state, false); -}); diff --git a/scripts/webui-performance/replay-turn.test.mjs b/scripts/webui-performance/replay-turn.test.mjs deleted file mode 100644 index 62802963e..000000000 --- a/scripts/webui-performance/replay-turn.test.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { resolve } from "node:path"; -import test from "node:test"; - -import { loadReplayTurn } from "./replay-turn.mjs"; - -test("replay turn loader validates exported stages and tool calls", () => { - const root = mkdtempSync(resolve(tmpdir(), "akashic-replay-turn-")); - const path = resolve(root, "turn.json"); - writeFileSync(path, JSON.stringify([ - { role: "user", content: "question" }, - { - role: "assistant", - content: "answer", - tool_chain: [{ - text: "stage text", - reasoning_content: "thinking", - calls: [{ - call_id: "call-1", name: "probe", status: "success", - arguments: { value: 1 }, final_arguments: { value: 1 }, result: "done", - }], - }], - }, - ])); - try { - assert.deepEqual(loadReplayTurn(path), { - content: "answer", - stages: [{ - text: "stage text", - reasoning: "thinking", - calls: [{ - callId: "call-1", name: "probe", status: "success", - arguments: { value: 1 }, finalArguments: { value: 1 }, result: "done", - }], - }], - }); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); - -test("replay turn loader rejects malformed tool arguments", () => { - const root = mkdtempSync(resolve(tmpdir(), "akashic-replay-turn-invalid-")); - const path = resolve(root, "turn.json"); - writeFileSync(path, JSON.stringify([{ - role: "assistant", content: "answer", - tool_chain: [{ calls: [{ - call_id: "call-1", name: "probe", status: "success", - arguments: [], final_arguments: {}, result: "done", - }] }], - }])); - try { - assert.throws(() => loadReplayTurn(path), /invalid arguments/u); - } finally { - rmSync(root, { recursive: true, force: true }); - } -}); diff --git a/tests/benchmark/test_harbor_v4flash_campaign.py b/tests/benchmark/test_harbor_v4flash_campaign.py deleted file mode 100644 index 8eac30ca9..000000000 --- a/tests/benchmark/test_harbor_v4flash_campaign.py +++ /dev/null @@ -1,128 +0,0 @@ -import json -from pathlib import Path - -import pytest - -from benchmark.harbor_v4flash.campaign import ( - CampaignGateError, - find_open_concurrency_gate, - task_slug, - validate_campaign_request, -) - - -def test_campaign_accepts_four_concurrent_tasks(tmp_path: Path) -> None: - tasks = [tmp_path / "one", tmp_path / "two"] - for task in tasks: - task.mkdir() - - validate_campaign_request(tasks, 4) - - -def test_campaign_rejects_more_than_four_concurrent_tasks(tmp_path: Path) -> None: - tasks = [tmp_path / "one", tmp_path / "two"] - for task in tasks: - task.mkdir() - - with pytest.raises(ValueError, match="1 到 4"): - validate_campaign_request(tasks, 5) - - -def test_campaign_rejects_duplicate_task_instances(tmp_path: Path) -> None: - task = tmp_path / "same" - task.mkdir() - - with pytest.raises(ValueError, match="重复"): - validate_campaign_request([task, task], 2) - - -def test_open_gate_requires_completed_stopped_isolated_smoke( - tmp_path: Path, -) -> None: - trial = tmp_path / "akasic-bench-v4flash-smoke-one" - trial.mkdir() - manifest = trial / "campaign-manifest.json" - manifest.write_text( - json.dumps( - { - "state": "completed", - "trial_name": "smoke-one", - "source": {"digest_after": "sha256:source"}, - "online": {"status": "passed"}, - "docker": {"all_stopped": True}, - "concurrency_gate": {"opened": True, "max_concurrent": 4}, - } - ), - encoding="utf-8", - ) - - gate = find_open_concurrency_gate( - tmp_path, - expected_source_digest="sha256:source", - ) - - assert gate["manifest"] == str(manifest) - assert gate["source_digest"] == "sha256:source" - - -def test_open_gate_fails_closed_without_smoke(tmp_path: Path) -> None: - with pytest.raises(CampaignGateError): - find_open_concurrency_gate( - tmp_path, - expected_source_digest="sha256:source", - ) - - -def test_open_gate_rejects_smoke_from_different_source(tmp_path: Path) -> None: - trial = tmp_path / "akasic-bench-v4flash-smoke-stale" - trial.mkdir() - (trial / "campaign-manifest.json").write_text( - json.dumps( - { - "state": "completed", - "trial_name": "smoke-stale", - "source": {"digest_after": "sha256:old-source"}, - "online": {"status": "passed"}, - "docker": {"all_stopped": True}, - "concurrency_gate": {"opened": True, "max_concurrent": 4}, - } - ), - encoding="utf-8", - ) - - with pytest.raises(CampaignGateError, match="sha256:new-source"): - find_open_concurrency_gate( - tmp_path, - expected_source_digest="sha256:new-source", - ) - - -def test_open_gate_rejects_old_three_concurrent_authorization( - tmp_path: Path, -) -> None: - trial = tmp_path / "akasic-bench-v4flash-smoke-old-limit" - trial.mkdir() - (trial / "campaign-manifest.json").write_text( - json.dumps( - { - "state": "completed", - "trial_name": "smoke-old-limit", - "source": {"digest_after": "sha256:source"}, - "online": {"status": "passed"}, - "docker": {"all_stopped": True}, - "concurrency_gate": {"opened": True, "max_concurrent": 3}, - } - ), - encoding="utf-8", - ) - - with pytest.raises(CampaignGateError, match="concurrency=4"): - find_open_concurrency_gate( - tmp_path, - expected_source_digest="sha256:source", - ) - - -def test_task_slug_is_bounded_and_docker_safe(tmp_path: Path) -> None: - task = tmp_path / ("UPPER_case.with spaces-" + "x" * 80) - assert task_slug(task) == "upper-case-with-spaces-" + "x" * 25 diff --git a/tests/benchmark/test_harbor_v4flash_controller.py b/tests/benchmark/test_harbor_v4flash_controller.py deleted file mode 100644 index 56a974f09..000000000 --- a/tests/benchmark/test_harbor_v4flash_controller.py +++ /dev/null @@ -1,784 +0,0 @@ -import asyncio -import json -import os -import shutil -import subprocess -import tomllib -from pathlib import Path -from types import SimpleNamespace - -import pytest -from harbor.environments.base import ExecResult - -from benchmark.harbor_v4flash.agent import ( - _ENDPOINT, - _WORKSPACE, - _build_driver_command, - _build_gateway_command, - _prepare_verifier_runtime, - _run_driver_and_shutdown, - _start_gateway_with_resource_evidence, - _verifier_dependency_command, -) -from benchmark.harbor_v4flash.controller import ( - _accepted_campaign_outcomes, - _append_campaign_event, - _capture_candidate_digest, - _greedy_task_schedule, - _inspect_finished_project, - _rate_limit_backoff_sec, - _replay_timed_out_verifier, - _restore_greedy_schedule, - _seed_campaign_outcomes, - _task_agent_timeout_sec, - _task_set_identity, - _task_verifier_timeout_sec, - _verifier_timeout, - _write_campaign_results, -) -from benchmark.harbor_v4flash.credentials import credential_scope -from benchmark.harbor_v4flash.isolation import IsolationError, create_source_bundle -from benchmark.harbor_v4flash.resource_evidence import ( - RESOURCE_EVIDENCE_FILENAME, - resource_probe_command, -) -from benchmark.harbor_v4flash.runtime_driver import _write_driver_outcome - - -class _ScriptedEnvironment: - def __init__(self, *outcomes: ExecResult | BaseException) -> None: - self._outcomes = list(outcomes) - self.commands: list[str] = [] - - async def exec( - self, - *, - command: str, - timeout_sec: float, - user: str | int | None = None, - ) -> ExecResult: - self.commands.append(command) - outcome = self._outcomes.pop(0) - if isinstance(outcome, BaseException): - raise outcome - return outcome - - -class _ReplayResult: - def __init__(self) -> None: - self.exception_info = SimpleNamespace( - exception_type="VerifierTimeoutError", - exception_message="Verifier execution timed out after 900.0 seconds", - ) - self.verifier_result = None - - def model_dump_json(self, *, indent: int) -> str: - return json.dumps( - { - "exception": ( - None - if self.exception_info is None - else self.exception_info.exception_type - ), - "reward": ( - None - if self.verifier_result is None - else self.verifier_result.rewards - ), - }, - indent=indent, - ) - - -def _git(root: Path, *args: str) -> str: - result = subprocess.run( - ["git", "-C", str(root), *args], - check=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - return result.stdout.strip() - - -def _resource_result(*, oom_kill: int = 0) -> ExecResult: - return ExecResult( - return_code=0, - stdout=( - "cgroup_version=2\n" - "@@memory.max\n4294967296\n" - "@@memory.current\n268435456\n" - "@@memory.events\n" - f"low 0\nhigh 0\nmax 1\noom {oom_kill}\n" - f"oom_kill {oom_kill}\noom_group_kill 0\n" - "@@memory.peak\n4294967296\n" - ), - ) - - -def test_v4flash_uses_deepseek_max_and_provider_output_limit() -> None: - config_path = ( - Path(__file__).parents[2] / "benchmark" / "harbor_v4flash" / "config.toml" - ) - config = tomllib.loads(config_path.read_text(encoding="utf-8")) - command = _build_gateway_command() - - assert "llm" not in config - assert "memory" not in config - assert "--chat-model deepseek-v4-flash" in command - assert "--context-window 1000000 --reasoning-effort max" in command - assert "--api-key-env DEEPSEEK_API_KEY" in command - assert command.count("/api/settings/model") == 2 - assert "/api/chat/model-settings" not in command - assert config["agent"]["max_iterations"] == 0 - - -def test_benchmark_commands_inherit_terminal_task_workdir() -> None: - gateway_command = _build_gateway_command() - driver_command = _build_driver_command(900) - - assert _WORKSPACE == "/opt/akashic-workspace" - assert _ENDPOINT == "/opt/akashic-workspace/akashic.sock" - assert "cd /app" not in gateway_command - assert "cd /app" not in driver_command - assert gateway_command.startswith("mkdir -p /opt/akashic-workspace && env ") - assert "main.py veda-reset" in gateway_command - assert driver_command.startswith("PYTHONPATH=/opt/akashic/src:") - assert "--outcome /logs/agent/driver-outcome.json" in driver_command - - -def test_campaign_ledger_recovers_only_accepted_outcomes(tmp_path: Path) -> None: - ledger = tmp_path / "events.jsonl" - accepted = {"state": "completed", "reward": {"reward": 1.0}} - _append_campaign_event( - ledger, - {"event": "attempt_failed", "task": "/tasks/one", "outcome": {}}, - ) - _append_campaign_event( - ledger, - {"event": "accepted", "task": "/tasks/two", "outcome": accepted}, - ) - - assert _accepted_campaign_outcomes(ledger) == {"/tasks/two": accepted} - - -def test_campaign_results_are_derived_from_accepted_wal(tmp_path: Path) -> None: - tasks = [tmp_path / "one", tmp_path / "two"] - for task in tasks: - task.mkdir() - accepted = { - str(tasks[1].resolve()): { - "state": "completed", - "reward": {"reward": 1.0}, - } - } - - path = tmp_path / "accepted-results.json" - _write_campaign_results(path, tasks, accepted) - - payload = json.loads(path.read_text(encoding="utf-8")) - assert payload["accepted"] == 1 - assert payload["expected"] == 2 - assert payload["score"] == {"passed": 1, "total": 1, "pass_rate": 1.0} - assert payload["outcomes"] == [accepted[str(tasks[1].resolve())]] - - -def test_seed_campaign_requeues_provider_error_500_and_keeps_valid_zero( - tmp_path: Path, -) -> None: - tasks = [tmp_path / "caffe", tmp_path / "valid-zero"] - for task in tasks: - task.mkdir() - seed = tmp_path / "seed" - seed.mkdir() - (seed / "manifest.json").write_text( - json.dumps( - { - "campaign_id": "old-campaign", - "source_digest_before": "sha256:old", - "tasks": [str(task.resolve()) for task in tasks], - } - ) - ) - for task in tasks: - trial = tmp_path / f"trial-{task.name}" - agent = trial / "agent" - agent.mkdir(parents=True) - terminal = { - "status": "failed" if task.name == "caffe" else "completed", - "error": ( - { - "type": "provider_error", - "message": "Error code: 500 - Router.Unavailable", - "retryable": True, - } - if task.name == "caffe" - else None - ), - } - (agent / "turn-result.json").write_text(json.dumps({"terminal": terminal})) - (agent / "driver-outcome.json").write_text( - json.dumps( - {"status": "agent_failed" if task.name == "caffe" else "completed"} - ) - ) - _append_campaign_event( - seed / "events.jsonl", - { - "event": "accepted", - "task": str(task.resolve()), - "outcome": { - "state": "completed", - "trial_dir": str(trial), - "reward": {"reward": 0.0}, - }, - }, - ) - - included, report = _seed_campaign_outcomes(seed, tasks) - - assert set(included) == {str(tasks[1].resolve())} - assert report["included"] == 1 - assert report["excluded"] == [ - { - "task": str(tasks[0].resolve()), - "failure_class": "provider_transient", - } - ] - - -def test_rate_limit_backoff_is_exponential_with_stable_jitter() -> None: - first = _rate_limit_backoff_sec("/tasks/one", 1, 30) - second = _rate_limit_backoff_sec("/tasks/one", 2, 30) - - assert 30 <= first <= 37.5 - assert 60 <= second <= 67.5 - assert first == _rate_limit_backoff_sec("/tasks/one", 1, 30) - - -def test_greedy_schedule_runs_long_official_budgets_first(tmp_path: Path) -> None: - tasks = [tmp_path / "short", tmp_path / "long", tmp_path / "medium"] - for task, timeout in zip(tasks, (600, 3600, 1200), strict=True): - task.mkdir() - (task / "task.toml").write_text(f"[agent]\ntimeout_sec={timeout}\n") - - ordered, schedule = _greedy_task_schedule(tasks) - - assert [path.name for path in ordered] == ["long", "medium", "short"] - assert [item["estimated_duration_sec"] for item in schedule] == [3600, 1200, 600] - assert {item["basis"] for item in schedule} == {"task_agent_timeout"} - assert _restore_greedy_schedule(tasks, schedule) == ordered - - -def test_greedy_schedule_ignores_historical_artifacts(tmp_path: Path) -> None: - task = tmp_path / "official" - task.mkdir() - (task / "task.toml").write_text("[agent]\ntimeout_sec=900\n") - (tmp_path / "campaign-manifest.json").write_text( - json.dumps({"historical_duration_sec": 99999}) - ) - - _, schedule = _greedy_task_schedule([task]) - - assert schedule[0]["estimated_duration_sec"] == 900 - assert schedule[0]["basis"] == "task_agent_timeout" - - -def test_task_set_identity_freezes_order_and_marks_local_provenance( - tmp_path: Path, -) -> None: - tasks = [tmp_path / "one", tmp_path / "two"] - for task in tasks: - task.mkdir() - (task / "task.toml").write_text("[agent]\ntimeout_sec=900\n") - - identity = _task_set_identity( - tasks, - dataset_dir=tmp_path, - dataset_ref=None, - ) - - assert identity["task_count"] == 2 - assert identity["provenance"] == "unverified_local_copy" - assert str(identity["task_set_digest"]).startswith("sha256:") - - -def test_driver_timeout_outcome_is_machine_readable(tmp_path: Path) -> None: - outcome = tmp_path / "driver-outcome.json" - _write_driver_outcome( - outcome, - status="timed_out", - error=TimeoutError("official budget exhausted"), - ) - - payload = json.loads(outcome.read_text(encoding="utf-8")) - assert payload["status"] == "timed_out" - assert payload["error"]["type"] == "TimeoutError" - - -def test_driver_success_keeps_command_order_and_logs(tmp_path: Path) -> None: - environment = _ScriptedEnvironment( - ExecResult(return_code=0, stdout="driver complete\n"), - _resource_result(), - ExecResult(return_code=0, stdout="shutdown complete\n"), - ) - result = asyncio.run( - _run_driver_and_shutdown( - environment, # type: ignore[arg-type] - driver_command="driver", - driver_timeout_sec=5, - shutdown_command="shutdown", - logs_dir=tmp_path, - ) - ) - - assert result.stdout == "driver complete\n" - assert environment.commands == ["driver", resource_probe_command(), "shutdown"] - assert (tmp_path / "driver.stdout.log").read_text( - encoding="utf-8" - ) == "driver complete\n" - assert (tmp_path / "runtime.shutdown.log").read_text( - encoding="utf-8" - ) == "shutdown complete\n" - assert ( - json.loads((tmp_path / RESOURCE_EVIDENCE_FILENAME).read_text(encoding="utf-8"))[ - "classification" - ] - == "none" - ) - assert not (tmp_path / "driver.exception.log").exists() - - -def test_verifier_uv_is_prepared_after_agent_with_frozen_version() -> None: - test_script = """#!/bin/bash -apt-get update -uvx \\ - -p 3.13 \\ - -w torch==2.7.0 \\ - pytest /tests/test_outputs.py -""" - digest = "/app\n" + "a" * 64 + "\n" - environment = _ScriptedEnvironment( - ExecResult(return_code=0), - ExecResult(return_code=0, stdout=digest), - ExecResult(return_code=0, stdout="Resolved 5 packages\n"), - ExecResult(return_code=0, stdout=digest), - ) - - evidence = asyncio.run( - _prepare_verifier_runtime( - environment, # type: ignore[arg-type] - expected_uv_version="uv 0.9.5", - test_script=test_script, - ) - ) - - assert len(environment.commands) == 4 - command = environment.commands[0] - assert "/opt/akashic-runtime/uv" in command - assert "/root/.local/bin/uvx" in command - assert "uv tool run" in command - assert "uv 0.9.5" in command - assert "torch==2.7.0" in environment.commands[2] - assert "pytest /tests/test_outputs.py" not in environment.commands[2] - assert "python -c 'pass'" in environment.commands[2] - assert evidence["official_verifier_timeout_started"] is False - assert evidence["candidate_digest_before"] == evidence["candidate_digest_after"] - - -def test_verifier_dependency_command_keeps_pip_setup_outside_pytest() -> None: - command = _verifier_dependency_command( - "#!/bin/bash\npip install pytest==8.4.1\npython -m pytest /tests/test.py\n" - ) - - assert command is not None - assert "pip install pytest==8.4.1" in command - assert "python -m pytest" not in command - - -def test_driver_timeout_still_shuts_down_gateway_and_persists_evidence( - tmp_path: Path, -) -> None: - environment = _ScriptedEnvironment( - TimeoutError("driver exceeded deadline"), - _resource_result(oom_kill=1), - ExecResult(return_code=0, stdout="gateway stopped\n"), - ) - - with pytest.raises(TimeoutError, match="driver exceeded deadline"): - asyncio.run( - _run_driver_and_shutdown( - environment, # type: ignore[arg-type] - driver_command="driver", - driver_timeout_sec=5, - shutdown_command="shutdown", - logs_dir=tmp_path, - ) - ) - - assert environment.commands == ["driver", resource_probe_command(), "shutdown"] - assert (tmp_path / "driver.stdout.log").read_text(encoding="utf-8") == "" - assert (tmp_path / "driver.stderr.log").read_text(encoding="utf-8") == "" - assert (tmp_path / "driver.exception.log").read_text( - encoding="utf-8" - ) == "TimeoutError: driver exceeded deadline\n" - assert (tmp_path / "runtime.shutdown.log").read_text( - encoding="utf-8" - ) == "gateway stopped\n" - assert ( - json.loads((tmp_path / RESOURCE_EVIDENCE_FILENAME).read_text(encoding="utf-8"))[ - "classification" - ] - == "resource_limit" - ) - - -def test_driver_cancellation_still_shuts_down_gateway( - tmp_path: Path, -) -> None: - environment = _ScriptedEnvironment( - asyncio.CancelledError("trial cancelled"), - _resource_result(), - ExecResult(return_code=0), - ) - - with pytest.raises(asyncio.CancelledError, match="trial cancelled"): - asyncio.run( - _run_driver_and_shutdown( - environment, # type: ignore[arg-type] - driver_command="driver", - driver_timeout_sec=5, - shutdown_command="shutdown", - logs_dir=tmp_path, - ) - ) - - assert environment.commands == ["driver", resource_probe_command(), "shutdown"] - assert (tmp_path / "driver.exception.log").read_text( - encoding="utf-8" - ) == "CancelledError: trial cancelled\n" - assert (tmp_path / "runtime.shutdown.log").read_text(encoding="utf-8") == "" - - -def test_driver_failure_remains_primary_when_shutdown_also_fails( - tmp_path: Path, -) -> None: - environment = _ScriptedEnvironment( - ExecResult(return_code=1, stdout="driver failed\n"), - RuntimeError("cgroup probe failed"), - ExecResult(return_code=2), - ) - - with pytest.raises(RuntimeError, match="执行 SDK turn") as caught: - asyncio.run( - _run_driver_and_shutdown( - environment, # type: ignore[arg-type] - driver_command="driver", - driver_timeout_sec=5, - shutdown_command="shutdown", - logs_dir=tmp_path, - ) - ) - - assert any("gateway cleanup also failed" in note for note in caught.value.__notes__) - assert any( - "resource evidence collection also failed" in note - for note in caught.value.__notes__ - ) - assert ( - (tmp_path / "driver.exception.log") - .read_text(encoding="utf-8") - .startswith("RuntimeError: 执行 SDK turn") - ) - assert (tmp_path / "runtime.shutdown.log").read_text(encoding="utf-8") == "exit=2\n" - resource = json.loads( - (tmp_path / RESOURCE_EVIDENCE_FILENAME).read_text(encoding="utf-8") - ) - assert resource["status"] == "collection_failed" - assert resource["classification"] == "unknown" - - -def test_resource_probe_failure_fails_loud_after_successful_driver( - tmp_path: Path, -) -> None: - environment = _ScriptedEnvironment( - ExecResult(return_code=0, stdout="driver complete\n"), - ExecResult(return_code=23, stderr="required cgroup file missing\n"), - ExecResult(return_code=0, stdout="gateway stopped\n"), - ) - - with pytest.raises(RuntimeError, match="采集容器资源证据"): - asyncio.run( - _run_driver_and_shutdown( - environment, # type: ignore[arg-type] - driver_command="driver", - driver_timeout_sec=5, - shutdown_command="shutdown", - logs_dir=tmp_path, - ) - ) - - assert environment.commands == ["driver", resource_probe_command(), "shutdown"] - resource = json.loads( - (tmp_path / RESOURCE_EVIDENCE_FILENAME).read_text(encoding="utf-8") - ) - assert resource["status"] == "collection_failed" - assert resource["classification"] == "unknown" - assert (tmp_path / "runtime.shutdown.log").read_text( - encoding="utf-8" - ) == "gateway stopped\n" - - -def test_secure_gateway_failure_keeps_primary_error_and_resource_evidence( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - startup_error = RuntimeError("secure docker exec failed") - environment = _ScriptedEnvironment(_resource_result(oom_kill=1)) - - async def fail_secure_exec(*args: object, **kwargs: object) -> ExecResult: - raise startup_error - - monkeypatch.setattr( - "benchmark.harbor_v4flash.agent.secure_docker_exec", - fail_secure_exec, - ) - - with pytest.raises(RuntimeError, match="secure docker exec failed") as caught: - asyncio.run( - _start_gateway_with_resource_evidence( - environment, # type: ignore[arg-type] - gateway_command="start-gateway", - credential_names=("DEEPSEEK_API_KEY", "DASHSCOPE_API_KEY"), - logs_dir=tmp_path, - ) - ) - - assert caught.value is startup_error - assert environment.commands == [resource_probe_command()] - resource = json.loads( - (tmp_path / RESOURCE_EVIDENCE_FILENAME).read_text(encoding="utf-8") - ) - assert resource["status"] == "collected" - assert resource["classification"] == "resource_limit" - - -def test_task_agent_timeout_uses_harbor_task_budget(tmp_path: Path) -> None: - task_dir = tmp_path / "task" - task_dir.mkdir() - (task_dir / "task.toml").write_text( - "[agent]\ntimeout_sec = 3600.0\n", - encoding="utf-8", - ) - - assert _task_agent_timeout_sec(task_dir) == 3600.0 - - -def test_task_verifier_timeout_uses_official_task_budget(tmp_path: Path) -> None: - task_dir = tmp_path / "task" - task_dir.mkdir() - (task_dir / "task.toml").write_text( - "[verifier]\ntimeout_sec = 900.0\n", - encoding="utf-8", - ) - - assert _task_verifier_timeout_sec(task_dir) == 900.0 - - -def test_candidate_digest_is_persisted_before_verifier(tmp_path: Path) -> None: - environment = _ScriptedEnvironment( - ExecResult(return_code=0, stdout="/workspace\nabc123\n") - ) - trial = SimpleNamespace(agent_environment=environment) - - asyncio.run( - _capture_candidate_digest(trial, tmp_path, SimpleNamespace()) # type: ignore[arg-type] - ) - - identity = json.loads( - (tmp_path / "agent" / "candidate-identity.json").read_text(encoding="utf-8") - ) - assert identity == { - "schema": "akasic.verifier-candidate.v1", - "root": "/workspace", - "digest": "sha256:abc123", - } - - -def test_verifier_timeout_replays_same_candidate_without_model_sampling( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - result = _ReplayResult() - (tmp_path / "agent").mkdir() - (tmp_path / "verifier").mkdir() - (tmp_path / "result.json").write_text("original\n", encoding="utf-8") - (tmp_path / "agent" / "candidate-identity.json").write_text( - json.dumps( - { - "schema": "akasic.verifier-candidate.v1", - "root": "/app", - "digest": "sha256:abc123", - } - ), - encoding="utf-8", - ) - docker_calls: list[tuple[str, ...]] = [] - - async def docker_command(*args: str, timeout_sec: float = 60) -> str: - docker_calls.append(args) - if args[0] == "exec": - return "/app\nabc123\n" - return "" - - async def run_process( - command: list[str], *, timeout_sec: float - ) -> tuple[int, str, bool]: - assert command[-1] == "(/tests/test.sh)" - assert timeout_sec == 900.0 - (tmp_path / "verifier" / "reward.txt").write_text("1\n", encoding="utf-8") - return 0, "6 passed\n", False - - monkeypatch.setattr( - "benchmark.harbor_v4flash.controller._docker_command", - docker_command, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.controller._run_process", - run_process, - ) - - replay = asyncio.run( - _replay_timed_out_verifier( - result, - trial_dir=tmp_path, - containers=[{"id": "container-id"}], - verifier_timeout_sec=900.0, - ) - ) - - assert _verifier_timeout(result) is False - assert result.exception_info is None - assert result.verifier_result.rewards == {"reward": 1.0} - assert replay is not None and replay["reward"] == 1.0 - assert docker_calls[0] == ("start", "container-id") - assert docker_calls[-1] == ("stop", "--time", "30", "container-id") - assert (tmp_path / "verifier-replay" / "original-result.json").read_text( - encoding="utf-8" - ) == "original\n" - - -def test_credential_scope_keeps_values_out_of_host_environment( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - profile = tmp_path / "config.toml" - profile.write_text( - """ -[credentials] -DEEPSEEK_API_KEY = "deepseek-sentinel" -DASHSCOPE_API_KEY = "dashscope-sentinel" -""".strip(), - encoding="utf-8", - ) - monkeypatch.setenv("DEEPSEEK_API_KEY", "previous") - monkeypatch.delenv("DASHSCOPE_API_KEY", raising=False) - - with credential_scope(profile) as names: - assert names == ("DASHSCOPE_API_KEY", "DEEPSEEK_API_KEY") - assert os.environ["DEEPSEEK_API_KEY"] == "previous" - assert "DASHSCOPE_API_KEY" not in os.environ - - assert os.environ["DEEPSEEK_API_KEY"] == "previous" - assert "DASHSCOPE_API_KEY" not in os.environ - - -@pytest.mark.parametrize("value", ["0", "-1", "nan", "inf"]) -def test_task_agent_timeout_rejects_invalid_budget( - tmp_path: Path, - value: str, -) -> None: - task_dir = tmp_path / value - task_dir.mkdir() - (task_dir / "task.toml").write_text( - f"[agent]\ntimeout_sec = {value}\n", - encoding="utf-8", - ) - - with pytest.raises(ValueError, match=r"\[agent\]\.timeout_sec"): - _task_agent_timeout_sec(task_dir) - - -def test_harbor_startup_failure_keeps_original_result( - monkeypatch: pytest.MonkeyPatch, -) -> None: - result = type("Result", (), {"exception_info": object()})() - - def missing_project(project_name: str) -> list[dict[str, object]]: - raise IsolationError(f"未找到 compose project:{project_name}") - - monkeypatch.setattr( - "benchmark.harbor_v4flash.controller.inspect_compose_project", - missing_project, - ) - - containers, error = _inspect_finished_project(result, "akasic-bench-missing") - - assert containers == [] - assert error == "未找到 compose project:akasic-bench-missing" - - -def test_success_without_compose_project_fails_loud( - monkeypatch: pytest.MonkeyPatch, -) -> None: - result = type("Result", (), {"exception_info": None})() - - def missing_project(project_name: str) -> list[dict[str, object]]: - raise IsolationError(f"未找到 compose project:{project_name}") - - monkeypatch.setattr( - "benchmark.harbor_v4flash.controller.inspect_compose_project", - missing_project, - ) - - with pytest.raises(IsolationError, match="未找到 compose project"): - _inspect_finished_project(result, "akasic-bench-missing") - - -def test_source_bundle_restores_history_and_keeps_worktree_overlay( - tmp_path: Path, -) -> None: - source = tmp_path / "source" - source.mkdir() - _git(source, "init") - _git(source, "config", "user.name", "Benchmark Test") - _git(source, "config", "user.email", "benchmark@localhost") - tracked = source / "tracked.txt" - tracked.write_text("baseline\n", encoding="utf-8") - _git(source, "add", "tracked.txt") - _git(source, "commit", "-m", "baseline") - baseline = _git(source, "rev-parse", "HEAD") - tracked.write_text("head\n", encoding="utf-8") - _git(source, "commit", "-am", "head") - head = _git(source, "rev-parse", "HEAD") - tracked.write_text("dirty overlay\n", encoding="utf-8") - - bundle = tmp_path / "inputs" / "source.bundle" - info = create_source_bundle(source, bundle) - - restored = tmp_path / "restored" - restored.mkdir() - _git(restored, "init") - shutil.copyfile(tracked, restored / "tracked.txt") - _git( - restored, - "fetch", - str(bundle), - "+refs/heads/*:refs/remotes/benchmark/*", - ) - _git(restored, "reset", "--mixed", head) - assert _git(restored, "cat-file", "-t", baseline) == "commit" - assert _git(restored, "rev-parse", "HEAD") == head - assert (restored / "tracked.txt").read_text(encoding="utf-8") == "dirty overlay\n" - assert _git(restored, "status", "--short") == "M tracked.txt" - assert info["head"] == head diff --git a/tests/benchmark/test_harbor_v4flash_credentials.py b/tests/benchmark/test_harbor_v4flash_credentials.py deleted file mode 100644 index 9feb15b20..000000000 --- a/tests/benchmark/test_harbor_v4flash_credentials.py +++ /dev/null @@ -1,99 +0,0 @@ -import asyncio -from pathlib import Path -from types import SimpleNamespace -from typing import Any - -import pytest - -from benchmark.harbor_v4flash.credentials import ( - credential_scope, - secure_docker_exec, -) - - -class _CompletedProcess: - returncode = 0 - - async def communicate(self) -> tuple[bytes, bytes]: - return b"credentials received\n", b"" - - def kill(self) -> None: - raise AssertionError("成功路径不应终止 docker 客户端") - - async def wait(self) -> int: - return self.returncode - - -def test_secure_exec_argv_hides_values_and_subprocess_receives_them( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - profile = tmp_path / "config.toml" - profile.write_text( - """ -[credentials] -DEEPSEEK_API_KEY = "deepseek-sentinel" -DASHSCOPE_API_KEY = "dashscope-sentinel" -""".strip(), - encoding="utf-8", - ) - captured: dict[str, Any] = {} - - async def create_subprocess_exec( - *argv: str, - **kwargs: object, - ) -> _CompletedProcess: - captured["argv"] = argv - captured["env"] = kwargs["env"] - return _CompletedProcess() - - monkeypatch.setattr( - "benchmark.harbor_v4flash.credentials._main_container_id", - lambda project: "container-id", - ) - monkeypatch.setattr(asyncio, "create_subprocess_exec", create_subprocess_exec) - environment = SimpleNamespace( - session_id="akasic-bench-v4flash-secret__env", - default_user="root", - task_env_config=SimpleNamespace(workdir="/app"), - ) - - with credential_scope(profile) as names: - result = asyncio.run( - secure_docker_exec( - environment, # type: ignore[arg-type] - command="run-gateway", - credential_names=names, - ) - ) - - argv = tuple(str(value) for value in captured["argv"]) - joined_argv = "\0".join(argv) - child_env = captured["env"] - assert result.return_code == 0 - assert "deepseek-sentinel" not in joined_argv - assert "dashscope-sentinel" not in joined_argv - assert not any(value.startswith("DEEPSEEK_API_KEY=") for value in argv) - assert not any(value.startswith("DASHSCOPE_API_KEY=") for value in argv) - assert argv.count("--env") == 2 - assert "DEEPSEEK_API_KEY" in argv - assert "DASHSCOPE_API_KEY" in argv - assert child_env["DEEPSEEK_API_KEY"] == "deepseek-sentinel" - assert child_env["DASHSCOPE_API_KEY"] == "dashscope-sentinel" - - -def test_secure_exec_fails_loud_without_credential_scope() -> None: - environment = SimpleNamespace( - session_id="akasic-bench-v4flash-secret__env", - default_user=None, - task_env_config=SimpleNamespace(workdir="/app"), - ) - - with pytest.raises(RuntimeError, match="scope 未激活"): - asyncio.run( - secure_docker_exec( - environment, # type: ignore[arg-type] - command="run-gateway", - credential_names=("DEEPSEEK_API_KEY",), - ) - ) diff --git a/tests/benchmark/test_harbor_v4flash_git_volume.py b/tests/benchmark/test_harbor_v4flash_git_volume.py deleted file mode 100644 index 49c771b1c..000000000 --- a/tests/benchmark/test_harbor_v4flash_git_volume.py +++ /dev/null @@ -1,89 +0,0 @@ -import subprocess - -import pytest - -from benchmark.harbor_v4flash.git_volume import ( - GIT_MOUNT_PATH, - GIT_TOP_LEVEL, - GitVolumeError, - _find_reusable_git_volume, - create_git_manifest, - git_volume_labels, -) - - -def test_git_manifest_freezes_builder_packages_and_content() -> None: - manifest = create_git_manifest( - builder_image={ - "reference": "debian:bullseye-slim", - "id": "sha256:builder", - "repo_digests": ["debian@sha256:repo"], - "platform": "linux/amd64", - }, - metadata={ - "git_version": "git version 2.30.2", - "git_package": "1:2.30.2-1", - "ca_certificates_package": "20210119", - }, - content_digest="sha256:content", - ) - - assert manifest["volume_name"].startswith("akasic-bench-git-v1-") - assert manifest["contents"] == { - "mount_path": GIT_MOUNT_PATH, - "git_path": "bin/git", - "top_level": list(GIT_TOP_LEVEL), - "contains_source": False, - "contains_workspace": False, - "contains_task_data": False, - "contains_secrets": False, - } - labels = git_volume_labels(manifest) - assert labels["akasic.benchmark.git.content_digest"] == "sha256:content" - assert labels["akasic.benchmark.git.git_version"] == "git version 2.30.2" - - -def test_git_volume_cache_reuses_valid_local_volume( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - "benchmark.harbor_v4flash.git_volume._run", - lambda _: subprocess.CompletedProcess( - args=[], - returncode=0, - stdout="akasic-bench-git-v1-valid\n", - stderr="", - ), - ) - expected = {"name": "akasic-bench-git-v1-valid"} - monkeypatch.setattr( - "benchmark.harbor_v4flash.git_volume.inspect_git_volume", - lambda _: expected, - ) - - assert _find_reusable_git_volume({"id": "sha256:builder"}) == expected - - -def test_git_volume_cache_exposes_corrupt_local_volume( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - "benchmark.harbor_v4flash.git_volume._run", - lambda _: subprocess.CompletedProcess( - args=[], - returncode=0, - stdout="akasic-bench-git-v1-corrupt\n", - stderr="", - ), - ) - - def reject(_: str) -> dict[str, object]: - raise GitVolumeError("corrupt") - - monkeypatch.setattr( - "benchmark.harbor_v4flash.git_volume.inspect_git_volume", - reject, - ) - - with pytest.raises(GitVolumeError, match="损坏"): - _find_reusable_git_volume({"id": "sha256:builder"}) diff --git a/tests/benchmark/test_harbor_v4flash_image_cache.py b/tests/benchmark/test_harbor_v4flash_image_cache.py deleted file mode 100644 index 635b05f5f..000000000 --- a/tests/benchmark/test_harbor_v4flash_image_cache.py +++ /dev/null @@ -1,152 +0,0 @@ -import asyncio -import threading -from pathlib import Path - -import pytest - -from benchmark.harbor_v4flash.image_cache import ( - MAX_IMAGE_PULL_CONCURRENCY, - TaskImageError, - _pull_image, - prefetch_task_images, - task_image_reference, -) - - -def test_task_image_reference_requires_prebuilt_image(tmp_path: Path) -> None: - task = tmp_path / "task" - task.mkdir() - (task / "task.toml").write_text( - """ -schema_version = "1.1" -[task] -name = "test/task" -[environment] -docker_image = "example/task:fixed" -""".strip(), - encoding="utf-8", - ) - - assert task_image_reference(task) == "example/task:fixed" - - -def test_task_image_reference_rejects_missing_image(tmp_path: Path) -> None: - task = tmp_path / "task" - task.mkdir() - (task / "task.toml").write_text( - """ -schema_version = "1.1" -[task] -name = "test/task" -[environment] -""".strip(), - encoding="utf-8", - ) - - with pytest.raises(TaskImageError, match="docker_image"): - task_image_reference(task) - - -def test_pull_image_reuses_complete_local_image( - monkeypatch: pytest.MonkeyPatch, -) -> None: - identity = { - "reference": "example/task:fixed", - "id": "sha256:image", - "repo_digests": ["example/task@sha256:repo"], - "platform": "linux/amd64", - "size_bytes": 123, - } - monkeypatch.setattr( - "benchmark.harbor_v4flash.image_cache._inspect_image", - lambda _: identity, - ) - - assert _pull_image("example/task:fixed") == { - **identity, - "cache_hit": True, - "pull_attempts": 0, - } - - -def test_prefetch_limits_registry_concurrency( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - active = 0 - peak = 0 - lock = threading.Lock() - release = threading.Event() - tasks = [] - for index in range(4): - task = tmp_path / f"task-{index}" - task.mkdir() - (task / "task.toml").write_text( - f""" -schema_version = "1.1" -[task] -name = "test/task-{index}" -[environment] -docker_image = "example/task-{index}:fixed" -""".strip(), - encoding="utf-8", - ) - tasks.append(task) - - def pull(reference: str) -> dict[str, object]: - nonlocal active, peak - with lock: - active += 1 - peak = max(peak, active) - if peak == MAX_IMAGE_PULL_CONCURRENCY: - release.set() - assert release.wait(timeout=5) - with lock: - active -= 1 - return {"reference": reference, "id": "sha256:image"} - - monkeypatch.setattr( - "benchmark.harbor_v4flash.image_cache._pull_image", - pull, - ) - - result = asyncio.run(prefetch_task_images(tasks)) - - assert peak == MAX_IMAGE_PULL_CONCURRENCY - assert set(result) == {str(path.resolve()) for path in tasks} - - -def test_prefetch_pulls_shared_reference_once( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - tasks = [] - for index in range(2): - task = tmp_path / f"task-{index}" - task.mkdir() - (task / "task.toml").write_text( - f""" -schema_version = "1.1" -[task] -name = "test/task-{index}" -[environment] -docker_image = "example/shared:fixed" -""".strip(), - encoding="utf-8", - ) - tasks.append(task) - calls: list[str] = [] - - def pull(reference: str) -> dict[str, object]: - calls.append(reference) - return {"reference": reference, "id": "sha256:image"} - - monkeypatch.setattr( - "benchmark.harbor_v4flash.image_cache._pull_image", - pull, - ) - - result = asyncio.run(prefetch_task_images(tasks)) - - assert calls == ["example/shared:fixed"] - assert set(result) == {str(path.resolve()) for path in tasks} diff --git a/tests/benchmark/test_harbor_v4flash_isolation.py b/tests/benchmark/test_harbor_v4flash_isolation.py deleted file mode 100644 index 8207e2944..000000000 --- a/tests/benchmark/test_harbor_v4flash_isolation.py +++ /dev/null @@ -1,396 +0,0 @@ -import ipaddress -import json -import subprocess -from pathlib import Path -from typing import cast - -import pytest - -from benchmark.harbor_v4flash.isolation import ( - IsolationError, - artifact_digests, - cleanup_compose_project, - compose_project_name, - inspect_compose_project, - require_storage_capacity, - reserve_compose_network, - sha256_file, - source_tree_digest, - stop_and_cleanup_compose_project, - validate_isolation, -) -from benchmark.harbor_v4flash.runtime_volume import RUNTIME_MOUNT_PATH - - -def _container( - *, - source: str, - ports: dict[str, object] | None = None, - volume_name: str = "akasic-bench-runtime-v1-fixed", - volume_rw: bool = False, -) -> dict[str, object]: - return { - "id": "container", - "name": "trial-client-1", - "image": "task:fixed", - "status": "running", - "running": True, - "project": "akasic-bench-v4flash-smoke__env", - "mounts": [ - { - "type": "bind", - "source": source, - "destination": "/logs/agent", - "rw": True, - }, - { - "type": "volume", - "name": volume_name, - "source": f"/var/lib/docker/volumes/{volume_name}/_data", - "destination": RUNTIME_MOUNT_PATH, - "rw": volume_rw, - }, - ], - "ports": ports or {}, - } - - -def test_compose_project_name_matches_harbor_normalization() -> None: - assert ( - compose_project_name("Akasic-Bench-V4Flash-Smoke.Name__env") - == "akasic-bench-v4flash-smoke-name__env" - ) - - -def test_reserve_compose_network_retries_overlapping_subnet( - monkeypatch: pytest.MonkeyPatch, -) -> None: - commands: list[list[str]] = [] - responses = iter( - [ - subprocess.CompletedProcess( - [], - 1, - stdout="", - stderr="Pool overlaps with other one on this address space", - ), - subprocess.CompletedProcess([], 0, stdout="network-id\n", stderr=""), - ] - ) - - def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - commands.append(command) - return next(responses) - - monkeypatch.setattr(subprocess, "run", run) - - network = reserve_compose_network( - "akasic-bench-v4flash-smoke__env", - network_pool=ipaddress.IPv4Network("10.240.0.0/29"), - network_prefix=30, - ) - - assert network["id"] == "network-id" - assert network["pool"] == "10.240.0.0/29" - assert commands[0][6] != commands[1][6] - - -def test_reserve_compose_network_rejects_non_benchmark_owner() -> None: - with pytest.raises(IsolationError, match="benchmark 前缀"): - reserve_compose_network("production_default") - - -def test_storage_capacity_fails_before_new_container( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - usage = type("Usage", (), {"total": 100, "used": 99, "free": 1})() - monkeypatch.setattr("shutil.disk_usage", lambda path: usage) - monkeypatch.setattr( - subprocess, - "run", - lambda *args, **kwargs: subprocess.CompletedProcess( - args[0], 0, "/var/lib/docker\n", "" - ), - ) - - with pytest.raises(IsolationError, match="停止调度"): - require_storage_capacity( - tmp_path, - min_runs_free_gib=1, - min_tmp_free_gib=1, - min_docker_free_gib=1, - ) - - -def test_cleanup_only_removes_exact_stopped_project( - monkeypatch: pytest.MonkeyPatch, -) -> None: - project = "akasic-bench-v4flash-smoke__env" - container = {"id": "container-id", "running": False} - calls: list[list[str]] = [] - - monkeypatch.setattr( - "benchmark.harbor_v4flash.isolation.inspect_compose_project", - lambda name: [container], - ) - - def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - calls.append(command) - if command[:3] == ["docker", "network", "inspect"]: - payload = [{ - "Id": "network-id", - "Name": "network-name", - "Labels": { - "com.docker.compose.project": project, - "akasic.benchmark.managed": "true", - }, - }] - return subprocess.CompletedProcess(command, 0, json.dumps(payload), "") - return subprocess.CompletedProcess(command, 0, "", "") - - monkeypatch.setattr(subprocess, "run", run) - result = cleanup_compose_project( - project, - expected_containers=[container], - network={"id": "network-id", "name": "network-name"}, - ) - - assert result["status"] == "removed" - assert ["docker", "container", "rm", "container-id"] in calls - assert ["docker", "network", "rm", "network-id"] in calls - - -def test_interruption_cleanup_stops_before_exact_project_removal( - monkeypatch: pytest.MonkeyPatch, -) -> None: - project = "akasic-bench-v4flash-smoke__env" - running = {"id": "container-id", "running": True} - stopped = {"id": "container-id", "running": False} - inspections = iter(([running], [stopped])) - calls: list[list[str]] = [] - - monkeypatch.setattr( - "benchmark.harbor_v4flash.isolation.inspect_compose_project", - lambda name: next(inspections), - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.isolation.cleanup_compose_project", - lambda name, *, expected_containers, network: { - "status": "removed", - "container_ids": [expected_containers[0]["id"]], - }, - ) - - def run(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - calls.append(command) - return subprocess.CompletedProcess(command, 0, "container-id\n", "") - - monkeypatch.setattr(subprocess, "run", run) - - result = stop_and_cleanup_compose_project( - project, - network={"id": "network-id", "name": "network-name"}, - ) - - assert result["status"] == "removed" - assert calls == [ - ["docker", "container", "stop", "--time", "10", "container-id"] - ] - - -def test_inspect_compose_project_records_immutable_image_id( - monkeypatch: pytest.MonkeyPatch, -) -> None: - responses = iter( - [ - subprocess.CompletedProcess([], 0, stdout="container-id\n", stderr=""), - subprocess.CompletedProcess( - [], - 0, - stdout=json.dumps( - [ - { - "Id": "container-id", - "Name": "/trial-main-1", - "Image": "sha256:image-id", - "Config": { - "Image": "task:tag", - "Labels": { - "com.docker.compose.project": ( - "akasic-bench-v4flash-smoke__env" - ) - }, - }, - "State": { - "Status": "exited", - "Running": False, - "ExitCode": 137, - "OOMKilled": False, - }, - "Mounts": [ - { - "Type": "volume", - "Name": "akasic-bench-runtime-v1-fixed", - "Source": ( - "/var/lib/docker/volumes/" - "akasic-bench-runtime-v1-fixed/_data" - ), - "Destination": RUNTIME_MOUNT_PATH, - "RW": False, - } - ], - "HostConfig": { - "PortBindings": {}, - "Memory": 4294967296, - }, - } - ] - ), - stderr="", - ), - ] - ) - monkeypatch.setattr(subprocess, "run", lambda *_, **__: next(responses)) - - containers = inspect_compose_project("akasic-bench-v4flash-smoke__env") - - assert containers[0]["image"] == "task:tag" - assert containers[0]["image_id"] == "sha256:image-id" - assert containers[0]["exit_code"] == 137 - assert containers[0]["oom_killed"] is False - assert containers[0]["memory_limit_bytes"] == 4294967296 - mounts = cast(list[dict[str, object]], containers[0]["mounts"]) - assert mounts[0]["name"] == ("akasic-bench-runtime-v1-fixed") - - -def test_validate_isolation_accepts_only_trial_bind_mounts(tmp_path: Path) -> None: - trial = tmp_path / "trial" - logs = trial / "agent" - logs.mkdir(parents=True) - project = "akasic-bench-v4flash-smoke__env" - - report = validate_isolation( - [_container(source=str(logs))], - project_name=project, - allowed_bind_root=trial, - forbidden_host_paths=[tmp_path / "online"], - allowed_volume_mounts=[("akasic-bench-runtime-v1-fixed", RUNTIME_MOUNT_PATH)], - ) - - assert report["status"] == "passed" - assert report["checked_bind_mounts"] == 1 - assert report["checked_volume_mounts"] == 1 - - -@pytest.mark.parametrize( - ("source", "ports"), - [ - ("/home/huashen/.akashic/workspace", {}), - ("/var/run/docker.sock", {}), - ("/tmp/allowed/agent", {"6322/tcp": [{"HostPort": "6322"}]}), - ], -) -def test_validate_isolation_rejects_host_escape( - tmp_path: Path, - source: str, - ports: dict[str, object], -) -> None: - allowed = Path("/tmp/allowed") - project = "akasic-bench-v4flash-smoke__env" - - with pytest.raises(IsolationError): - validate_isolation( - [_container(source=source, ports=ports)], - project_name=project, - allowed_bind_root=allowed, - forbidden_host_paths=[Path("/home/huashen/.akashic/workspace")], - allowed_volume_mounts=[ - ("akasic-bench-runtime-v1-fixed", RUNTIME_MOUNT_PATH) - ], - ) - - -@pytest.mark.parametrize( - ("volume_name", "volume_rw"), - [ - ("other-volume", False), - ("akasic-bench-runtime-v1-fixed", True), - ], -) -def test_validate_isolation_rejects_unapproved_or_writable_volume( - volume_name: str, - volume_rw: bool, -) -> None: - project = "akasic-bench-v4flash-smoke__env" - - with pytest.raises(IsolationError): - validate_isolation( - [ - _container( - source="/tmp/allowed/agent", - volume_name=volume_name, - volume_rw=volume_rw, - ) - ], - project_name=project, - allowed_bind_root=Path("/tmp/allowed"), - forbidden_host_paths=[], - allowed_volume_mounts=[ - ("akasic-bench-runtime-v1-fixed", RUNTIME_MOUNT_PATH) - ], - ) - - -def test_validate_isolation_rejects_missing_runtime_volume() -> None: - project = "akasic-bench-v4flash-smoke__env" - container = _container(source="/tmp/allowed/agent") - mounts = cast(list[dict[str, object]], container["mounts"]) - container["mounts"] = mounts[:1] - - with pytest.raises(IsolationError, match="缺少 allowlist volume"): - validate_isolation( - [container], - project_name=project, - allowed_bind_root=Path("/tmp/allowed"), - forbidden_host_paths=[], - allowed_volume_mounts=[ - ("akasic-bench-runtime-v1-fixed", RUNTIME_MOUNT_PATH) - ], - ) - - -def test_artifact_digests_excludes_self_referential_manifest( - tmp_path: Path, -) -> None: - manifest = tmp_path / "campaign-manifest.json" - trace = tmp_path / "agent" / "trace.jsonl" - trace.parent.mkdir() - manifest.write_text('{"state":"prepared"}\n', encoding="utf-8") - trace.write_text('{"event":"completed"}\n', encoding="utf-8") - - digests = artifact_digests(tmp_path, exclude={manifest}) - - assert "campaign-manifest.json" not in digests - assert digests == {"agent/trace.jsonl": sha256_file(trace)} - - -def test_source_digest_ignores_gitignored_reports_but_keeps_dirty_overlay( - tmp_path: Path, -) -> None: - subprocess.run(["git", "init", str(tmp_path)], check=True, capture_output=True) - tracked = tmp_path / "tracked.txt" - tracked.write_text("one\n", encoding="utf-8") - (tmp_path / ".gitignore").write_text("reports/\n", encoding="utf-8") - subprocess.run( - ["git", "-C", str(tmp_path), "add", "tracked.txt", ".gitignore"], - check=True, - ) - baseline = source_tree_digest(tmp_path) - report = tmp_path / "reports" / "gate.json" - report.parent.mkdir() - report.write_text("ignored\n", encoding="utf-8") - assert source_tree_digest(tmp_path) == baseline - - tracked.write_text("dirty\n", encoding="utf-8") - assert source_tree_digest(tmp_path) != baseline diff --git a/tests/benchmark/test_harbor_v4flash_resource_evidence.py b/tests/benchmark/test_harbor_v4flash_resource_evidence.py deleted file mode 100644 index 0f54b9218..000000000 --- a/tests/benchmark/test_harbor_v4flash_resource_evidence.py +++ /dev/null @@ -1,136 +0,0 @@ -import json -from pathlib import Path - -import pytest - -from benchmark.harbor_v4flash.resource_evidence import ( - RESOURCE_EVIDENCE_FILENAME, - load_resource_evidence, - parse_resource_probe_output, - resource_probe_command, -) - - -def test_resource_probe_command_reads_only_fixed_cgroup_memory_files() -> None: - command = resource_probe_command() - - assert "/sys/fs/cgroup/memory.max" in command - assert "/sys/fs/cgroup/memory.current" in command - assert "/sys/fs/cgroup/memory.events" in command - assert "/proc/" not in command - assert "journalctl" not in command - assert "docker" not in command - - -def test_resource_probe_classifies_cgroup_oom_kill_as_resource_limit() -> None: - evidence = parse_resource_probe_output(""" -cgroup_version=2 -@@memory.max -4294967296 -@@memory.current -1073741824 -@@memory.events -low 0 -high 0 -max 532 -oom 3 -oom_kill 1 -oom_group_kill 0 -@@memory.peak -4294967296 -@@memory.events.local -low 0 -high 0 -max 532 -oom 3 -oom_kill 1 -oom_group_kill 0 -""".lstrip()) - - assert evidence["status"] == "collected" - assert evidence["classification"] == "resource_limit" - memory = evidence["cgroup"]["memory"] # type: ignore[index] - assert memory["limit_bytes"] == 4294967296 - assert memory["current_bytes"] == 1073741824 - assert memory["peak_bytes"] == 4294967296 - assert memory["events"]["oom_kill"] == 1 - - -def test_resource_probe_classifies_oom_without_kill_as_resource_limit() -> None: - evidence = parse_resource_probe_output(""" -cgroup_version=2 -@@memory.max -max -@@memory.current -2048 -@@memory.events -low 0 -high 0 -max 1 -oom 1 -oom_kill 0 -oom_group_kill 0 -""".lstrip()) - - assert evidence["classification"] == "resource_limit" - assert evidence["cgroup"]["memory"]["limit_bytes"] is None # type: ignore[index] - - -@pytest.mark.parametrize( - "output", - [ - "", - "cgroup_version=1\n", - "cgroup_version=2\n@@memory.max\n4096\n", - ( - "cgroup_version=2\n@@memory.max\n4096\n" - "@@memory.current\ninvalid\n@@memory.events\noom_kill 1\n" - ), - ( - "cgroup_version=2\n@@memory.max\n4096\n" - "@@memory.current\n1024\n@@memory.events\nnot-valid\n" - ), - ], -) -def test_resource_probe_rejects_incomplete_or_malformed_evidence( - output: str, -) -> None: - with pytest.raises(ValueError): - parse_resource_probe_output(output) - - -def test_missing_or_corrupt_resource_artifact_is_explicit( - tmp_path: Path, -) -> None: - path = tmp_path / RESOURCE_EVIDENCE_FILENAME - - missing = load_resource_evidence(path) - assert missing["status"] == "unavailable" - assert missing["classification"] == "unknown" - - path.write_text("{not-json", encoding="utf-8") - corrupt = load_resource_evidence(path) - assert corrupt["status"] == "collection_failed" - assert corrupt["classification"] == "unknown" - - -def test_load_resource_evidence_preserves_valid_failure_artifact( - tmp_path: Path, -) -> None: - path = tmp_path / RESOURCE_EVIDENCE_FILENAME - path.write_text( - json.dumps( - { - "schema": "akasic.container-resource.v1", - "status": "collection_failed", - "classification": "unknown", - "error": {"type": "RuntimeError", "message": "probe failed"}, - } - ), - encoding="utf-8", - ) - - evidence = load_resource_evidence(path) - - assert evidence["status"] == "collection_failed" - assert evidence["error"]["type"] == "RuntimeError" # type: ignore[index] diff --git a/tests/benchmark/test_harbor_v4flash_result_projection.py b/tests/benchmark/test_harbor_v4flash_result_projection.py deleted file mode 100644 index 0d25f23ad..000000000 --- a/tests/benchmark/test_harbor_v4flash_result_projection.py +++ /dev/null @@ -1,72 +0,0 @@ -from types import SimpleNamespace - -from benchmark.harbor_v4flash.result_projection import project_agent_context - - -def test_project_agent_context_initializes_optional_harbor_metadata() -> None: - context = SimpleNamespace( - n_input_tokens=None, - n_cache_tokens=None, - n_output_tokens=None, - metadata=None, - ) - result = { - "thread_id": "programmatic:test", - "turn_id": "turn:test", - "status": "completed", - "terminal_source": "turn/read_recovery", - "event_count": 26, - "terminal": { - "usage": { - "inputTokens": 162497, - "cachedInputTokens": 148224, - "outputTokens": 3992, - } - }, - } - - project_agent_context( - context, - result, - harness_name="akasic-v4flash", - harness_version="0.1.0", - source_digest="sha256:test", - ) - - assert context.n_input_tokens == 162497 - assert context.n_cache_tokens == 148224 - assert context.n_output_tokens == 3992 - assert context.metadata["terminal_source"] == "turn/read_recovery" - assert context.metadata["event_count"] == 26 - assert context.metadata["usage_available"] is True - - -def test_project_agent_context_allows_failed_turn_without_usage() -> None: - context = SimpleNamespace( - n_input_tokens=None, - n_cache_tokens=None, - n_output_tokens=None, - metadata=None, - ) - result = { - "thread_id": "programmatic:failed", - "turn_id": "turn:failed", - "status": "failed", - "terminal_source": "event", - "event_count": 4, - "terminal": {"usage": None}, - } - - project_agent_context( - context, - result, - harness_name="akasic-v4flash", - harness_version="0.1.0", - source_digest="sha256:test", - ) - - assert context.n_input_tokens is None - assert context.n_cache_tokens is None - assert context.n_output_tokens is None - assert context.metadata["turn_status"] == "failed" - assert context.metadata["usage_available"] is False diff --git a/tests/benchmark/test_harbor_v4flash_runtime_driver.py b/tests/benchmark/test_harbor_v4flash_runtime_driver.py deleted file mode 100644 index 3c8d6cc5a..000000000 --- a/tests/benchmark/test_harbor_v4flash_runtime_driver.py +++ /dev/null @@ -1,391 +0,0 @@ -import asyncio -import json -from pathlib import Path -from typing import Any - -import pytest -from akashic_sdk import SlowConsumerError - -from benchmark.harbor_v4flash.runtime_driver import ( - AgentTurnFailed, - ProviderAccountLimited, - ProviderRateLimited, - ProviderTransientFailure, - TurnDeadlineExceeded, - _driver_error_status, - _observe_terminal, - _turn_was_empty_provider_response, - _turn_was_account_limited, - _turn_was_rate_limited, - _turn_was_transient_provider_failure, -) - - -class _HandleWithoutTerminalEvent: - thread_id = "programmatic:test" - id = "turn:test" - - async def events(self): - yield {"method": "turn/started", "params": {"turnId": self.id}} - await asyncio.Event().wait() - - -class _PersistedTerminalClient: - async def turn_read(self, thread_id: str, turn_id: str) -> dict[str, Any]: - return { - "id": turn_id, - "threadId": thread_id, - "status": "completed", - } - - -class _DelayedPersistedTerminalClient(_PersistedTerminalClient): - async def turn_read(self, thread_id: str, turn_id: str) -> dict[str, Any]: - await asyncio.sleep(0.01) - return await super().turn_read(thread_id, turn_id) - - -class _BurstHandle: - thread_id = "programmatic:burst" - id = "turn:burst" - - def __init__(self) -> None: - self.queue: asyncio.Queue[dict[str, Any]] = asyncio.Queue(512) - - async def events(self): - yield {"method": "turn/started", "params": {"turnId": self.id}} - while True: - event = await self.queue.get() - yield event - if event.get("method") == "turn/completed": - return - - -class _BurstBeforeReadResponseClient: - def __init__(self, handle: _BurstHandle) -> None: - self.handle = handle - self.terminal = { - "id": handle.id, - "threadId": handle.thread_id, - "status": "completed", - } - - async def turn_read(self, thread_id: str, turn_id: str) -> dict[str, Any]: - assert thread_id == self.handle.thread_id - assert turn_id == self.handle.id - for index in range(600): - try: - self.handle.queue.put_nowait( - { - "method": "turn/output_delta", - "params": {"turnId": turn_id, "delta": str(index)}, - } - ) - except asyncio.QueueFull as error: - raise SlowConsumerError("turn notification queue overflow") from error - await asyncio.sleep(0) - self.handle.queue.put_nowait( - { - "method": "turn/completed", - "params": {"turnId": turn_id, "turn": self.terminal}, - } - ) - await asyncio.sleep(0) - return self.terminal - - -class _TerminalThenReadErrorClient: - def __init__(self, handle: _BurstHandle) -> None: - self.handle = handle - - async def turn_read(self, thread_id: str, turn_id: str) -> dict[str, Any]: - self.handle.queue.put_nowait( - { - "method": "turn/completed", - "params": { - "turnId": turn_id, - "turn": { - "id": turn_id, - "threadId": thread_id, - "status": "completed", - }, - }, - } - ) - await asyncio.sleep(0) - raise RuntimeError("turn read failed") - - -class _EndedStreamHandle: - thread_id = "programmatic:ended" - id = "turn:ended" - - async def events(self): - yield {"method": "turn/started", "params": {"turnId": self.id}} - - -class _FailingStreamHandle: - thread_id = "programmatic:failed-stream" - id = "turn:failed-stream" - - async def events(self): - yield {"method": "turn/started", "params": {"turnId": self.id}} - raise RuntimeError("event stream failed") - - -class _CancellingHandle: - thread_id = "programmatic:cancel" - id = "turn:cancel" - - def __init__(self) -> None: - self.closed = asyncio.Event() - - async def events(self): - try: - yield {"method": "turn/started", "params": {"turnId": self.id}} - await asyncio.Event().wait() - finally: - self.closed.set() - - -class _NeverReturningClient: - async def turn_read(self, thread_id: str, turn_id: str) -> dict[str, Any]: - await asyncio.Event().wait() - raise AssertionError("unreachable") - - -def test_driver_error_status_keeps_readiness_failure_out_of_valid_timeout() -> None: - assert _driver_error_status(TimeoutError("readiness")) == "infra_failed" - assert _driver_error_status(TurnDeadlineExceeded("budget")) == "timed_out" - assert _driver_error_status(AgentTurnFailed("turn")) == "agent_failed" - assert _driver_error_status(ProviderRateLimited("429")) == "rate_limited" - assert _driver_error_status(ProviderTransientFailure("500")) == "provider_transient" - assert _driver_error_status(ProviderAccountLimited("quota")) == "account_limited" - - -def test_rate_limit_detection_only_reads_structured_turn_error() -> None: - assert _turn_was_rate_limited( - { - "status": "failed", - "input": "unrelated", - "error": { - "type": "RateLimitError", - "message": "Error code: 429 - Too Many Requests", - "retryable": True, - }, - } - ) - assert not _turn_was_rate_limited( - { - "status": "failed", - "input": "please explain HTTP 429 rate limits", - "error": {"type": "RuntimeError", "message": "tool failed"}, - } - ) - - -def test_provider_transient_detection_requires_provider_type_and_explicit_5xx() -> None: - assert _turn_was_transient_provider_failure( - { - "error": { - "type": "InternalServerError", - "message": "Error code: 500 - Router.Unavailable", - } - } - ) - assert _turn_was_transient_provider_failure( - { - "error": { - "type": "provider_error", - "message": ( - "Error code: 500 - {'type': 'Router.Unavailable', " - "'modelID': 'deepseek-v4-flash'}" - ), - "retryable": True, - } - } - ) - assert not _turn_was_transient_provider_failure( - { - "error": { - "type": "RuntimeError", - "message": "tool returned status code: 500", - } - } - ) - - -def test_provider_transient_detection_accepts_incomplete_response_body() -> None: - assert _turn_was_transient_provider_failure( - { - "error": { - "type": "RemoteProtocolError", - "message": ( - "peer closed connection without sending complete message body " - "(incomplete chunked read)" - ), - "retryable": False, - } - } - ) - assert not _turn_was_transient_provider_failure( - { - "error": { - "type": "RuntimeError", - "message": "incomplete chunked read", - } - } - ) - - -def test_empty_provider_response_requires_runtime_fallback_without_tool_calls() -> None: - terminal = { - "status": "completed", - "finalResponse": "模型未返回可用回复,请重试。", - "error": None, - "items": [ - {"type": "userMessage", "data": {"content": "task"}}, - { - "type": "assistantMessage", - "data": { - "content": "模型未返回可用回复,请重试。", - "metadata": {"streamed_reply": False}, - }, - }, - ], - } - - assert _turn_was_empty_provider_response(terminal) - terminal["items"].insert(1, {"type": "toolCall", "data": {"name": "shell"}}) - assert not _turn_was_empty_provider_response(terminal) - - -def test_go_usage_limit_is_not_treated_as_ordinary_rate_limit() -> None: - turn = { - "error": { - "type": "RateLimitError", - "message": "GoUsageLimitError: 5 hour usage limit reached", - } - } - - assert _turn_was_account_limited(turn) - assert _turn_was_rate_limited(turn) - - -@pytest.mark.asyncio -async def test_observer_recovers_persisted_terminal_after_delivery_gap( - tmp_path: Path, -) -> None: - trace = tmp_path / "trace.jsonl" - - terminal, source, event_count = await _observe_terminal( - _PersistedTerminalClient(), - _HandleWithoutTerminalEvent(), - trace_path=trace, - turn_timeout_s=1, - poll_interval_s=0.001, - terminal_grace_s=0.003, - ) - - records = [ - json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines() - ] - assert terminal["status"] == "completed" - assert source == "turn/read_recovery" - assert event_count == 1 - assert records[-1]["phase"] == "terminal_recovered" - assert records[-1]["delivery_gap"] is True - - -@pytest.mark.asyncio -async def test_observer_continuously_drains_burst_while_turn_read_is_pending( - tmp_path: Path, -) -> None: - trace = tmp_path / "trace.jsonl" - handle = _BurstHandle() - - terminal, source, event_count = await _observe_terminal( - _BurstBeforeReadResponseClient(handle), - handle, - trace_path=trace, - turn_timeout_s=1, - poll_interval_s=0.001, - ) - - records = [ - json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines() - ] - assert terminal["status"] == "completed" - assert source == "event" - assert event_count == 602 - assert [record["event"]["method"] for record in records] == [ - "turn/started", - *(["turn/output_delta"] * 600), - "turn/completed", - ] - - -@pytest.mark.asyncio -async def test_observer_recovers_terminal_after_event_stream_ends( - tmp_path: Path, -) -> None: - trace = tmp_path / "trace.jsonl" - - terminal, source, event_count = await _observe_terminal( - _DelayedPersistedTerminalClient(), - _EndedStreamHandle(), - trace_path=trace, - turn_timeout_s=1, - poll_interval_s=0.001, - ) - - assert terminal["status"] == "completed" - assert source == "turn/read_recovery" - assert event_count == 1 - - -@pytest.mark.asyncio -async def test_observer_preserves_event_stream_error_priority(tmp_path: Path) -> None: - with pytest.raises(RuntimeError, match="event stream failed"): - await _observe_terminal( - _PersistedTerminalClient(), - _FailingStreamHandle(), - trace_path=tmp_path / "trace.jsonl", - turn_timeout_s=1, - poll_interval_s=0.001, - ) - - -@pytest.mark.asyncio -async def test_observer_preserves_turn_read_error_priority(tmp_path: Path) -> None: - handle = _BurstHandle() - - with pytest.raises(RuntimeError, match="turn read failed"): - await _observe_terminal( - _TerminalThenReadErrorClient(handle), - handle, - trace_path=tmp_path / "trace.jsonl", - turn_timeout_s=1, - poll_interval_s=0.001, - ) - - -@pytest.mark.asyncio -async def test_observer_cancellation_closes_event_drain(tmp_path: Path) -> None: - handle = _CancellingHandle() - observer = asyncio.create_task( - _observe_terminal( - _NeverReturningClient(), - handle, - trace_path=tmp_path / "trace.jsonl", - turn_timeout_s=10, - poll_interval_s=0.001, - ) - ) - await asyncio.sleep(0.01) - - observer.cancel() - with pytest.raises(asyncio.CancelledError): - await observer - - assert handle.closed.is_set() diff --git a/tests/benchmark/test_harbor_v4flash_runtime_volume.py b/tests/benchmark/test_harbor_v4flash_runtime_volume.py deleted file mode 100644 index 07f5c36b5..000000000 --- a/tests/benchmark/test_harbor_v4flash_runtime_volume.py +++ /dev/null @@ -1,298 +0,0 @@ -import hashlib -import json -import subprocess -from pathlib import Path -from typing import Any, cast - -import pytest - -from benchmark.harbor_v4flash.runtime_volume import ( - DEFAULT_BUILDER_IMAGE, - DEFAULT_PYTHON_VERSION, - RUNTIME_MOUNT_PATH, - RUNTIME_TOP_LEVEL, - RuntimeVolumeError, - _builder_image_identity, - _resolver_platform, - build_runtime_volume, - create_runtime_manifest, - inspect_runtime_volume, - runtime_compose_overlay, - runtime_volume_labels, -) - - -def _manifest() -> tuple[dict[str, Any], bytes]: - lock_bytes = b"example==1.0 --hash=sha256:abc\n" - manifest = cast( - dict[str, Any], - create_runtime_manifest( - requirements={ - "source": "requirements.txt", - "source_digest": "sha256:requirements-source", - "extras": ["tzdata"], - "digest": "sha256:requirements", - }, - uv={ - "version": "uv 0.8.23", - "digest": "sha256:uv", - }, - python_version=DEFAULT_PYTHON_VERSION, - platform="linux/amd64", - resolver_platform="x86_64-manylinux_2_28", - builder_image={ - "reference": "debian:bullseye-slim", - "id": "sha256:builder", - "repo_digests": ["debian@sha256:repo"], - "platform": "linux/amd64", - "libc": "glibc 2.31", - }, - resolved_lock_digest=(f"sha256:{hashlib.sha256(lock_bytes).hexdigest()}"), - ), - ) - return manifest, lock_bytes - - -def test_runtime_manifest_and_compose_freeze_identity() -> None: - manifest, _ = _manifest() - volume_name = str(manifest["volume_name"]) - - assert volume_name.startswith("akasic-bench-runtime-v1-") - assert ( - runtime_volume_labels(manifest)["akasic.benchmark.runtime.resolved_lock_digest"] - == manifest["recipe"]["resolved_lock"]["digest"] - ) - assert ( - runtime_volume_labels(manifest)["akasic.benchmark.runtime.builder_glibc"] - == "glibc 2.31" - ) - assert runtime_compose_overlay(volume_name) == { - "services": { - "main": { - "volumes": [ - { - "type": "volume", - "source": "akasic_runtime", - "target": RUNTIME_MOUNT_PATH, - "read_only": True, - } - ] - } - }, - "volumes": { - "akasic_runtime": { - "external": True, - "name": volume_name, - } - }, - } - overlay = cast( - dict[str, Any], - runtime_compose_overlay( - volume_name, - task_image_id="sha256:task-image", - git_volume_name="akasic-bench-git-v1-example", - ), - ) - assert overlay["services"]["main"] == { - "image": "sha256:task-image", - "pull_policy": "never", - "volumes": [ - { - "type": "volume", - "source": "akasic_runtime", - "target": RUNTIME_MOUNT_PATH, - "read_only": True, - }, - { - "type": "volume", - "source": "akasic_git", - "target": "/opt/akashic-git", - "read_only": True, - }, - ], - } - - -def test_runtime_labels_reject_manifest_without_builder_glibc() -> None: - manifest, _ = _manifest() - del manifest["recipe"]["builder_image"]["libc"] - - with pytest.raises(RuntimeVolumeError, match="builder glibc 缺失"): - runtime_volume_labels(manifest) - - -def test_inspect_runtime_volume_verifies_manifest_lock_and_inputs( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - manifest, lock_bytes = _manifest() - volume_name = str(manifest["volume_name"]) - labels = runtime_volume_labels(manifest) - requirements = manifest["recipe"]["requirements"] - uv = manifest["recipe"]["uv"] - - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._inspect_volume", - lambda _: { - "Driver": "local", - "Scope": "local", - "CreatedAt": "fixed", - "Labels": labels, - }, - ) - - def read_file(**kwargs: str) -> bytes: - if kwargs["relative_path"] == "manifest.json": - return json.dumps(manifest).encode() - return lock_bytes - - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._read_volume_file", - read_file, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._volume_top_level", - lambda *_: list(RUNTIME_TOP_LEVEL), - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._docker_platform", - lambda: "linux/amd64", - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._requirements_identity", - lambda _: requirements, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._uv_identity", - lambda _: uv, - ) - - report = inspect_runtime_volume( - volume_name, - source_root=tmp_path, - uv_binary=tmp_path / "uv", - ) - - assert report["name"] == volume_name - assert report["manifest"] == manifest - - -def test_inspect_runtime_volume_rejects_label_mismatch( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - manifest, _ = _manifest() - volume_name = str(manifest["volume_name"]) - labels = runtime_volume_labels(manifest) - labels["akasic.benchmark.runtime.uv_version"] = "uv changed" - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._inspect_volume", - lambda _: {"Labels": labels}, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._read_volume_file", - lambda **_: json.dumps(manifest).encode(), - ) - - with pytest.raises(RuntimeVolumeError, match="labels"): - inspect_runtime_volume( - volume_name, - source_root=tmp_path, - uv_binary=tmp_path / "uv", - ) - - -def test_builder_rejects_non_frozen_python_before_docker( - tmp_path: Path, -) -> None: - with pytest.raises(RuntimeVolumeError, match="已冻结"): - build_runtime_volume( - source_root=tmp_path, - uv_binary=tmp_path / "uv", - python_version="3.13.8", - builder_image_reference="debian:bookworm-slim", - ) - - -def test_runtime_resolver_uses_explicit_manylinux_baseline() -> None: - assert DEFAULT_BUILDER_IMAGE == "debian:bullseye-slim" - assert _resolver_platform("linux/amd64") == "x86_64-manylinux_2_28" - assert _resolver_platform("linux/arm64") == "aarch64-manylinux_2_28" - - -def test_builder_image_id_reuses_local_immutable_image( - monkeypatch: pytest.MonkeyPatch, -) -> None: - commands: list[list[str]] = [] - - def run(command: list[str], *, text: bool = True): - commands.append(command) - if command[:3] == ["docker", "image", "inspect"]: - return subprocess.CompletedProcess( - command, - 0, - stdout=json.dumps( - [ - { - "Id": "sha256:builder", - "RepoDigests": ["debian@sha256:repo"], - "Os": "linux", - "Architecture": "amd64", - } - ] - ), - stderr="", - ) - return subprocess.CompletedProcess( - command, - 0, - stdout="glibc 2.31\n", - stderr="", - ) - - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._run", - run, - ) - - identity = _builder_image_identity("sha256:builder") - - assert identity["id"] == "sha256:builder" - assert not any(command[:2] == ["docker", "pull"] for command in commands) - - -def test_builder_rejects_glibc_newer_than_official_task_floor( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._docker_platform", - lambda: "linux/amd64", - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._requirements_identity", - lambda _: {"digest": "sha256:requirements"}, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._uv_identity", - lambda _: {"version": "uv test", "digest": "sha256:uv"}, - ) - monkeypatch.setattr( - "benchmark.harbor_v4flash.runtime_volume._builder_image_identity", - lambda _: { - "reference": "debian:bookworm-slim", - "id": "sha256:builder", - "repo_digests": [], - "platform": "linux/amd64", - "libc": "glibc 2.36", - }, - ) - - with pytest.raises(RuntimeVolumeError, match="glibc 高于兼容上限"): - build_runtime_volume( - source_root=tmp_path, - uv_binary=tmp_path / "uv", - python_version=DEFAULT_PYTHON_VERSION, - builder_image_reference="debian:bookworm-slim", - ) diff --git a/tests/benchmark/test_harbor_v4flash_transition.py b/tests/benchmark/test_harbor_v4flash_transition.py deleted file mode 100644 index acdcb8f73..000000000 --- a/tests/benchmark/test_harbor_v4flash_transition.py +++ /dev/null @@ -1,114 +0,0 @@ -import json -from pathlib import Path - -from benchmark.harbor_v4flash import transition -from benchmark.harbor_v4flash.transition import ( - _controller_environment, - _signal_old_controller, - _terminal_task_names, -) - - -def test_terminal_task_names_only_accepts_campaign_terminal_events( - tmp_path: Path, -) -> None: - ledger = tmp_path / "events.jsonl" - events = [ - {"event": "attempt_started", "task": "/tasks/one"}, - {"event": "accepted", "task": "/tasks/two"}, - {"event": "attempt_failed", "task": "/tasks/three"}, - ] - ledger.write_text("\n".join(json.dumps(event) for event in events) + "\n") - - assert _terminal_task_names(ledger) == {"two", "three"} - - -def test_signal_old_controller_is_idempotent_when_unit_is_inactive( - monkeypatch, -) -> None: - monkeypatch.setattr(transition, "_unit_active", lambda unit: False) - - def unexpected_run(*args, **kwargs): - raise AssertionError("inactive unit must not receive another signal") - - monkeypatch.setattr(transition.subprocess, "run", unexpected_run) - - _signal_old_controller("old.service") - - -def test_signal_old_controller_targets_only_main_owner(monkeypatch) -> None: - calls = [] - monkeypatch.setattr(transition, "_unit_active", lambda unit: True) - monkeypatch.setattr( - transition.subprocess, - "run", - lambda command, **kwargs: calls.append((command, kwargs)), - ) - - _signal_old_controller("old.service") - - assert calls == [ - ( - [ - "systemctl", - "--user", - "kill", - "--kill-who=main", - "--signal=SIGINT", - "old.service", - ], - {"check": True}, - ) - ] - - -def test_cleanup_old_projects_uses_full_network_id(monkeypatch, tmp_path) -> None: - project = "akasic-bench-old__env" - network_id = "a" * 64 - commands = [] - cleaned = [] - monkeypatch.setattr( - transition, - "_old_source_projects", - lambda runs_dir, source_digest: [project], - ) - - def fake_run(command, **kwargs): - commands.append(command) - return transition.subprocess.CompletedProcess( - command, - 0, - stdout=f"{network_id}\t{project}_default\n", - ) - - monkeypatch.setattr(transition.subprocess, "run", fake_run) - monkeypatch.setattr( - transition, - "stop_and_cleanup_compose_project", - lambda project_name, *, network: cleaned.append((project_name, network)), - ) - - transition._cleanup_old_projects(tmp_path, "sha256:old") - - assert "--no-trunc" in commands[0] - assert cleaned == [ - ( - project, - {"id": network_id, "name": f"{project}_default"}, - ) - ] - - -def test_controller_environment_injects_fixed_source_and_sdk( - monkeypatch, - tmp_path, -) -> None: - monkeypatch.setenv("PYTHONPATH", "/existing/python") - - environment = _controller_environment(tmp_path) - - assert environment["PYTHONPATH"].split(transition.os.pathsep) == [ - str(tmp_path), - str(tmp_path / "sdk" / "python" / "src"), - "/existing/python", - ] diff --git a/tests/control/test_exec_cli.py b/tests/control/test_exec_cli.py deleted file mode 100644 index 29e2b2bbb..000000000 --- a/tests/control/test_exec_cli.py +++ /dev/null @@ -1,347 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -import os -import sys -from pathlib import Path - -import pytest - -from agent.control.client import ClientTurnHandle, ConnectionClosedError, ControlClient -from agent.control.models import TurnRequest -from agent.control.ports import ControlExecutionResult -from agent.control.runtime import ConversationRuntime -from agent.control.service import ControlService -from infra.control.socket import SocketAppServer -from session.manager import SessionManager - - -@pytest.mark.asyncio -async def test_control_client_unblocks_turn_stream_when_server_closes( - tmp_path: Path, -) -> None: - endpoint = tmp_path / "closing.sock" - - async def close_after_turn_start( - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - ) -> None: - initialize = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": initialize["id"], - "result": {"protocolVersion": "1.0"}, - } - ) - + "\n" - ).encode() - ) - await writer.drain() - _ = await reader.readline() - thread_start = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": thread_start["id"], - "result": {"id": "programmatic:closing"}, - } - ) - + "\n" - ).encode() - ) - await writer.drain() - turn_start = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": turn_start["id"], - "result": { - "id": "turn:closing", - "threadId": "programmatic:closing", - "status": "queued", - }, - } - ) - + "\n" - ).encode() - ) - await writer.drain() - writer.close() - await writer.wait_closed() - - server = await asyncio.start_unix_server(close_after_turn_start, path=endpoint) - client = await ControlClient.connect(str(endpoint)) - try: - thread = await client.start_thread() - handle = await client.start_turn(str(thread["id"]), "verify") - - with pytest.raises(ConnectionClosedError, match="server closed"): - _ = await asyncio.wait_for(anext(handle.events()), 1) - finally: - await client.close() - server.close() - await server.wait_closed() - - -@pytest.mark.asyncio -async def test_control_client_preserves_buffered_terminal_on_disconnect() -> None: - reader = asyncio.StreamReader() - client = ControlClient(reader, object()) # type: ignore[arg-type] - for index in range(511): - reader.feed_data( - ( - json.dumps( - { - "jsonrpc": "2.0", - "method": "item/completed", - "params": {"turnId": "turn:full", "index": index}, - } - ) - + "\n" - ).encode() - ) - reader.feed_data( - ( - json.dumps( - { - "jsonrpc": "2.0", - "method": "turn/completed", - "params": { - "turnId": "turn:full", - "turn": {"id": "turn:full", "status": "completed"}, - }, - } - ) - + "\n" - ).encode() - ) - reader.feed_eof() - - await client._read_loop() - handle = ClientTurnHandle(client, "programmatic:full", "turn:full", {}) - - assert await handle.result() == {"id": "turn:full", "status": "completed"} - - -@pytest.mark.asyncio -async def test_control_client_reads_terminal_larger_than_asyncio_default( - tmp_path: Path, -) -> None: - """长递归 turn 的 terminal frame 不得触发默认 64 KiB 断连。""" - - endpoint = tmp_path / "large-terminal.sock" - large_preview = "x" * (80 * 1024) - - async def send_large_terminal( - reader: asyncio.StreamReader, - writer: asyncio.StreamWriter, - ) -> None: - # 1. 完成 initialize 与 thread/turn start 握手。 - initialize = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": initialize["id"], - "result": {"protocolVersion": "1.0"}, - } - ) - + "\n" - ).encode() - ) - await writer.drain() - _ = await reader.readline() - thread_start = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": thread_start["id"], - "result": {"id": "programmatic:large"}, - } - ) - + "\n" - ).encode() - ) - await writer.drain() - turn_start = json.loads(await reader.readline()) - writer.write( - ( - json.dumps( - { - "jsonrpc": "2.0", - "id": turn_start["id"], - "result": { - "id": "turn:large", - "threadId": "programmatic:large", - "status": "queued", - }, - } - ) - + "\n" - ).encode() - ) - - # 2. 单帧超过 asyncio 默认值,但仍在 control 合同上限内。 - terminal = { - "jsonrpc": "2.0", - "method": "turn/completed", - "params": { - "turnId": "turn:large", - "turn": { - "id": "turn:large", - "status": "completed", - "finalResponse": "done", - "items": [{"resultPreview": large_preview}], - }, - }, - } - writer.write((json.dumps(terminal) + "\n").encode()) - await writer.drain() - writer.close() - await writer.wait_closed() - - server = await asyncio.start_unix_server(send_large_terminal, path=endpoint) - client = await ControlClient.connect(str(endpoint)) - try: - thread = await client.start_thread() - handle = await client.start_turn(str(thread["id"]), "verify") - - result = await asyncio.wait_for(handle.result(), 2) - - assert result["status"] == "completed" - assert result["items"][0]["resultPreview"] == large_preview - finally: - await client.close() - server.close() - await server.wait_closed() - - -@pytest.mark.asyncio -async def test_exec_remote_error_exits_two_without_traceback(tmp_path: Path) -> None: - sessions = SessionManager(tmp_path) - - async def execute(request: TurnRequest) -> ControlExecutionResult: - return ControlExecutionResult(response=request.input) - - runtime = ConversationRuntime(sessions.control_store, execute) - server = SocketAppServer( - tmp_path / "control.sock", - ControlService(runtime, sessions, tmp_path), - ) - await server.start() - try: - process = await asyncio.create_subprocess_exec( - sys.executable, - "main.py", - "exec", - "--thread", - "programmatic:missing", - "--endpoint", - str(server.endpoint), - "--workspace", - str(tmp_path), - "hello", - cwd=Path(__file__).resolve().parents[2], - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await asyncio.wait_for(process.communicate(), 10) - assert process.returncode == 2 - assert stdout == b"" - assert "thread" in stderr.decode() - assert "Traceback" not in stderr.decode() - finally: - await server.stop() - await runtime.shutdown() - sessions.close() - - -@pytest.mark.asyncio -async def test_exec_new_rejects_unbound_latest_and_defaults_to_stable( - tmp_path: Path, -) -> None: - sessions = SessionManager(tmp_path) - seen: list[TurnRequest] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - seen.append(request) - return ControlExecutionResult(response=request.input) - - runtime = ConversationRuntime(sessions.control_store, execute) - server = SocketAppServer( - tmp_path / "control.sock", - ControlService(runtime, sessions, tmp_path), - ) - await server.start() - - async def run( - *extra: str, - env: dict[str, str] | None = None, - ) -> tuple[int, str, str]: - process = await asyncio.create_subprocess_exec( - sys.executable, - "main.py", - "exec", - "--new", - "--endpoint", - str(server.endpoint), - "--workspace", - str(tmp_path), - *extra, - cwd=Path(__file__).resolve().parents[2], - env=env, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await asyncio.wait_for(process.communicate(), 10) - assert process.returncode is not None - return process.returncode, stdout.decode(), stderr.decode() - - try: - latest = await run("--runtime", "latest", "--final-only", "verify") - forged_env = os.environ.copy() - forged_env["AKASHIC_PLUGIN_ROLLOUT_OWNER_TURN"] = "turn:forged" - forged_owner = await run("--final-only", "forged-owner", env=forged_env) - persistent = await run("--persist-memory", "--final-only", "remember") - - assert latest[0] == 2 - assert latest[1] == "" - assert "attached 插件验证子 turn" in latest[2] - assert forged_owner == (0, "forged-owner\n", "") - assert persistent == (0, "remember\n", "") - rows = sessions.list_sessions() - assert seen[0].metadata["inboundMetadata"] == { - "effects": {"post_commit": "suppress"} - } - assert "inboundMetadata" not in seen[1].metadata - stored_metadata = { - str(row["key"]): sessions.control_store.get_session_meta( - str(row["key"]) - )["metadata"] - for row in rows - } - assert list(stored_metadata.values()).count( - {"effects": {"post_commit": "suppress"}} - ) == 1 - assert list(stored_metadata.values()).count( - {"effects": {"post_commit": "allow"}} - ) == 1 - assert [request.metadata["runtime"] for request in seen] == [ - "stable", - "stable", - ] - finally: - await server.stop() - await runtime.shutdown() - sessions.close() diff --git a/tests/control/test_plugin_turn_lineage.py b/tests/control/test_plugin_turn_lineage.py deleted file mode 100644 index 571ed6bbd..000000000 --- a/tests/control/test_plugin_turn_lineage.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from agent.control.models import TurnRequest -from agent.control.ports import ControlExecutionResult -from agent.control.runtime import ConversationRuntime -from agent.control.service import ControlService -from session.manager import SessionManager - - -@pytest.mark.asyncio -async def test_attached_programmatic_child_automatically_binds_frozen_latest( - tmp_path: Path, -) -> None: - observed: list[TurnRequest] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - observed.append(request) - return ControlExecutionResult(response="ok") - - sessions = SessionManager(tmp_path) - runtime = ConversationRuntime(sessions.control_store, execute) - - consumed = False - - def binding(capability: str, consume: bool) -> dict[str, str] | None: - nonlocal consumed - assert capability == "opaque-child-capability" - if consume and consumed: - return None - consumed = consume - return { - "runtime": "latest", - "ownerTurnId": "parent-1", - "pluginId": "fitbit@github", - "generationId": "gen-2", - "sourceRevision": "rev-2", - } - - service = ControlService( - runtime, - sessions, - tmp_path, - plugin_child_binding=binding, - ) - thread = service.start_thread({}, plugin_rollout_capability="opaque-child-capability") - handle = await service.start_turn( - str(thread["id"]), - "verify", - {}, - attached=True, - ) - await handle.result() - - assert observed[0].metadata["runtime"] == "latest" - assert observed[0].metadata["_pluginRolloutGenerationId"] == "gen-2" - await runtime.shutdown() - - -@pytest.mark.asyncio -async def test_detached_programmatic_child_capability_is_rejected( - tmp_path: Path, -) -> None: - observed: list[TurnRequest] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - observed.append(request) - return ControlExecutionResult(response="ok") - - sessions = SessionManager(tmp_path) - runtime = ConversationRuntime(sessions.control_store, execute) - service = ControlService( - runtime, - sessions, - tmp_path, - plugin_child_binding=lambda _capability, _consume: { - "runtime": "latest", - "ownerTurnId": "parent-1", - "pluginId": "fitbit@github", - "generationId": "gen-2", - "sourceRevision": "rev-2", - }, - ) - thread = service.start_thread({}, plugin_rollout_capability="opaque") - with pytest.raises(ValueError, match="必须 attached"): - await service.start_turn(str(thread["id"]), "verify", {}, attached=False) - - assert observed == [] - await runtime.shutdown() - - -@pytest.mark.asyncio -async def test_detached_programmatic_child_cannot_request_latest( - tmp_path: Path, -) -> None: - async def execute(_request: TurnRequest) -> ControlExecutionResult: - return ControlExecutionResult(response="unused") - - sessions = SessionManager(tmp_path) - runtime = ConversationRuntime(sessions.control_store, execute) - service = ControlService( - runtime, - sessions, - tmp_path, - plugin_child_binding=lambda _capability, _consume: None, - ) - thread = service.start_thread({}) - - with pytest.raises(ValueError, match="attached 插件验证子 turn"): - await service.start_turn( - str(thread["id"]), - "verify", - {}, - runtime="latest", - attached=False, - ) - await runtime.shutdown() - - -@pytest.mark.asyncio -async def test_rollout_metadata_forgery_and_capability_replay_are_rejected( - tmp_path: Path, -) -> None: - sessions = SessionManager(tmp_path) - - async def execute(_request: TurnRequest) -> ControlExecutionResult: - return ControlExecutionResult(response="unused") - - runtime = ConversationRuntime(sessions.control_store, execute) - consumed = False - - def binding(_capability: str, consume: bool) -> dict[str, str] | None: - nonlocal consumed - if not consume and consumed: - return None - if consume: - return { - "runtime": "latest", - "ownerTurnId": "parent-1", - "pluginId": "fitbit@github", - "generationId": "gen-2", - "sourceRevision": "rev-2", - } - consumed = True - return { - "runtime": "latest", - "ownerTurnId": "parent-1", - "pluginId": "fitbit@github", - "generationId": "gen-2", - "sourceRevision": "rev-2", - } - - service = ControlService(runtime, sessions, tmp_path, plugin_child_binding=binding) - with pytest.raises(ValueError, match="Core 保留"): - service.start_thread({"_pluginRolloutOwnerTurnId": "parent-1"}) - thread = service.start_thread({}, plugin_rollout_capability="opaque") - with pytest.raises(ValueError, match="已经使用"): - service.start_thread({}, plugin_rollout_capability="opaque") - with pytest.raises(ValueError, match="Core 保留"): - await service.start_turn( - str(thread["id"]), - "verify", - {"_pluginRolloutGenerationId": "gen-2"}, - ) - await runtime.shutdown() diff --git a/tests/control/test_timer_contract.py b/tests/control/test_timer_contract.py deleted file mode 100644 index 8d34423c7..000000000 --- a/tests/control/test_timer_contract.py +++ /dev/null @@ -1,77 +0,0 @@ -import asyncio -from datetime import UTC, datetime, timedelta - -import pytest - -from agent.control.timer import AsyncioOneShotTimer, TimerReceipt, TimerStatus - - -def test_timer_receipt_normalizes_aware_times_and_status() -> None: - deadline = datetime(2026, 8, 22, 20, 0, tzinfo=UTC) - receipt = TimerReceipt( - timer_id="timer:1", - deadline=deadline, - settled_at=deadline + timedelta(seconds=1), - status=TimerStatus.FIRED, - ) - - assert receipt.deadline == deadline - assert receipt.status is TimerStatus.FIRED - - -def test_timer_receipt_rejects_identity_and_naive_time() -> None: - aware = datetime(2026, 8, 22, 20, 0, tzinfo=UTC) - - with pytest.raises(ValueError, match="timer_id"): - TimerReceipt("", aware, aware, TimerStatus.CANCELLED) - with pytest.raises(ValueError, match="时区"): - TimerReceipt("timer:1", aware.replace(tzinfo=None), aware, TimerStatus.FIRED) - - -@pytest.mark.asyncio -async def test_asyncio_timer_fires_once_and_reuses_terminal_receipt() -> None: - now = datetime(2026, 8, 22, 12, tzinfo=UTC) - delays: list[float] = [] - - async def sleep(delay: float) -> None: - delays.append(delay) - - handle = AsyncioOneShotTimer(clock=lambda: now, sleeper=sleep).schedule( - now + timedelta(seconds=7) - ) - - first = await handle.result() - second = await handle.cancel() - - assert delays == [7.0] - assert first is second - assert first.status is TimerStatus.FIRED - - -@pytest.mark.asyncio -async def test_asyncio_timer_cancel_and_cleanup_leave_no_wait() -> None: - now = datetime(2026, 8, 22, 12, tzinfo=UTC) - sleeping = asyncio.Event() - - async def sleep(_delay: float) -> None: - sleeping.set() - await asyncio.Future() - - handle = AsyncioOneShotTimer(clock=lambda: now, sleeper=sleep).schedule( - now + timedelta(days=1) - ) - await sleeping.wait() - - cancelled = await handle.cancel() - await handle.cleanup() - - assert cancelled.status is TimerStatus.CANCELLED - assert await handle.result() is cancelled - - -@pytest.mark.asyncio -async def test_asyncio_timer_rejects_naive_deadline_before_task_creation() -> None: - timer = AsyncioOneShotTimer() - - with pytest.raises(ValueError, match="deadline"): - timer.schedule(datetime(2026, 8, 22, 12)) diff --git a/tests/control/test_workspace_lock.py b/tests/control/test_workspace_lock.py deleted file mode 100644 index 7d8ccc4be..000000000 --- a/tests/control/test_workspace_lock.py +++ /dev/null @@ -1,16 +0,0 @@ -from pathlib import Path - -import pytest - -from bootstrap.workspace_lock import WorkspaceInstanceLock - - -def test_workspace_lock_rejects_second_owner_and_releases(tmp_path: Path) -> None: - first = WorkspaceInstanceLock(tmp_path) - second = WorkspaceInstanceLock(tmp_path) - first.acquire() - with pytest.raises(RuntimeError, match="其他 runtime"): - second.acquire() - first.release() - second.acquire() - second.release() diff --git a/tests/mobile_realtime/test_config.py b/tests/mobile_realtime/test_config.py deleted file mode 100644 index a9188e28b..000000000 --- a/tests/mobile_realtime/test_config.py +++ /dev/null @@ -1,143 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import pytest - -from agent.config import load_config - -_BASE = """ -[agent] -system_prompt = "test" - -[channels.chat] -enabled = true -""" - - -def _write_config(tmp_path: Path, mobile: str) -> Path: - path = tmp_path / "config.toml" - path.write_text(_BASE + mobile, encoding="utf-8") - return path - - -def test_mobile_realtime_defaults_to_disabled(tmp_path: Path) -> None: - config = load_config(_write_config(tmp_path, ""), workspace=tmp_path) - - assert config.mobile_realtime.enabled is False - assert config.mobile_realtime.port == 6323 - assert str(config.mobile_realtime.key_encryption.keyset_manifest) == ( - "data/mobile/keys/current.json" - ) - - -def test_mobile_realtime_loads_strict_wss_and_key_encryption(tmp_path: Path) -> None: - config = load_config( - _write_config( - tmp_path, - """ -[mobile_realtime] -enabled = true -host = "0.0.0.0" -port = 6323 -database = "data/mobile.db" -lan_hostname = "agent.local" -public_url = "wss://agent.example.com/ws" -max_attachment_mb = 64 -inbox_retention_days = 9 - -[mobile_realtime.key_encryption] -provider = "secret_service" -master_key_namespace = "akasic/mobile-test" -keyset_manifest = "data/mobile/keys/current.json" - """, - ), - workspace=tmp_path, - ) - - assert config.mobile_realtime.enabled is True - assert config.mobile_realtime.public_url == "wss://agent.example.com/ws" - assert config.mobile_realtime.max_attachment_mb == 64 - assert config.mobile_realtime.inbox_retention.days == 9 - - -def test_mobile_realtime_loads_file_master_key_provider(tmp_path: Path) -> None: - config = load_config( - _write_config( - tmp_path, - """ -[mobile_realtime] -enabled = true - -[mobile_realtime.key_encryption] -provider = "file" -master_key_namespace = "" -master_key_file = "data/mobile/private/master-keys.json" - """, - ), - workspace=tmp_path, - ) - - assert config.mobile_realtime.key_encryption.provider == "file" - assert config.mobile_realtime.key_encryption.master_key_file == Path( - "data/mobile/private/master-keys.json" - ) - - -@pytest.mark.parametrize( - ("mobile", "message"), - [ - ( - """ -[mobile_realtime] -enabled = true -public_url = "ws://agent.example.com/ws" -""", - "public_url", - ), - ( - """ -[mobile_realtime] -enabled = true -database = "../outside.db" -""", - "安全相对路径", - ), - ( - """ -[mobile_realtime] -enabled = true -[mobile_realtime.key_encryption] -provider = "plaintext" -""", - "只支持 secret_service 或 file", - ), - ], -) -def test_mobile_realtime_rejects_unsafe_configuration( - tmp_path: Path, - mobile: str, - message: str, -) -> None: - with pytest.raises(ValueError, match=message): - load_config(_write_config(tmp_path, mobile), workspace=tmp_path) - - -def test_mobile_realtime_requires_enabled_webchat_pairing_entry( - tmp_path: Path, -) -> None: - config = _BASE.replace( - "[channels.chat]\nenabled = true", - "[channels.chat]\nenabled = false", - ) - path = tmp_path / "config.toml" - path.write_text( - config + """ -[mobile_realtime] -enabled = true -""", - encoding="utf-8", - ) - - with pytest.raises(ValueError, match="配对入口"): - load_config(path, workspace=tmp_path) diff --git a/tests/mobile_realtime/test_gateway.py b/tests/mobile_realtime/test_gateway.py deleted file mode 100644 index 349fa7ad1..000000000 --- a/tests/mobile_realtime/test_gateway.py +++ /dev/null @@ -1,3447 +0,0 @@ -from __future__ import annotations - -import asyncio -import base64 -import hashlib -import json -import logging -import secrets -import sqlite3 -from collections import deque -from contextlib import closing -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import AsyncMock -from uuid import uuid4 - -import pytest -import infra.mobile_realtime.gateway as gateway_module -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec -from fastapi.testclient import TestClient -from starlette.websockets import WebSocketDisconnect - -from agent.config_models import MobileKeyEncryptionConfig, MobileRealtimeConfig -from agent.plugins.mobile_ui import MobileUiRpcExecutionError -from agent.plugin_composition.channels import ( - ChannelFactoryContext, - ChannelInboundMessage, - ChannelRuntimePorts, - RawInbound, -) -from bus.events import OutboundMessage, channel_message_from_outbound -from bus.events_lifecycle import ( - StreamDeltaReady, - ToolCallCompleted, - ToolCallStarted, - TurnStarted, -) -from infra.channels.base import AttachmentStore -from infra.channels.artifacts import ChannelAttachmentArtifactStore -from infra.mobile_realtime.attachments import ( - AttachmentChunk, - AttachmentTransferService, - MAX_ATTACHMENT_CHUNK_BYTES, - attachment_descriptor, - decode_attachment_chunk, - encode_attachment_chunk, -) -from infra.mobile_realtime.auth import device_proof_signing_bytes -from infra.mobile_realtime.gateway import ( - ActiveMobileConnection, - build_mobile_gateway_runtime, - build_mobile_gateway_server, - create_mobile_gateway_app, -) -from infra.mobile_realtime.key_protection import KeyProtectionError -from infra.mobile_realtime.plugin_ui_http import ( - PluginUiHttpTicketError, - PluginUiHttpTicketIssuer, -) -from infra.mobile_realtime.protocol import ( - AttachmentDownloadCommand, - TURN_OUTPUT_COMPLETED_CAPABILITY, - parse_frame, -) -from infra.mobile_realtime.storage import DeviceRecord, MobileStorageError -from session.manager import SessionManager - - -async def _attach_open_mobile_v3( - channel: Any, - ingress: Any, - *, - binding_token: str, -) -> Any: - """Attach one exact v3 ingress and open admission for this fixture.""" - - context = ChannelFactoryContext( - snapshot_id="gateway-test-snapshot", - generation_id="gateway-test-generation", - binding_token=binding_token, - config={}, - credentials={}, - provider_client_factory=cast(Any, object()), - ingress=ingress, - identity=None, - ) - adapter = channel.build_v3_adapter(context) - adapter.attach_runtime( - ChannelRuntimePorts( - snapshot_id=context.snapshot_id, - generation_id=context.generation_id, - binding_token=context.binding_token, - ingress=context.ingress, - identity=context.identity, - attachment_import=context.attachment_import, - ) - ) - assert (await adapter.start()).binding_token == binding_token - adapter.open_admission() - return adapter - - -@pytest.mark.asyncio -async def test_gateway_publish_event_reports_zero_after_device_race( - tmp_path: Path, -) -> None: - runtime, _keyset = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key="test-public-key", - display_name="Pixel", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=(), - ) - ) - original_list_active_devices = runtime.storage.list_active_devices - calls = 0 - - def list_active_devices_with_race() -> tuple[DeviceRecord, ...]: - nonlocal calls - calls += 1 - if calls == 1: - return original_list_active_devices() - return () - - runtime.storage.list_active_devices = ( # type: ignore[method-assign] - list_active_devices_with_race - ) - try: - assert runtime.storage.list_active_devices() - recipient_count = await runtime.publish_event( - event_type="session.updated", - session_id="akashic:race", - payload={ - "session_id": "akashic:race", - "message_id": "akashic:race:0", - "head_seq": 0, - }, - ) - assert recipient_count == 0 - assert calls == 2 - assert runtime.storage.count_durable_events(device_id) == 0 - finally: - runtime.close() - - -class _ControlledWebSocket: - def __init__( - self, - *, - send_gate: asyncio.Event | None = None, - close_gate: asyncio.Event | None = None, - bytes_gate: asyncio.Event | None = None, - fail_send: bool = False, - ) -> None: - self.send_gate = send_gate - self.close_gate = close_gate - self.bytes_gate = bytes_gate - self.fail_send = fail_send - self.receive_hang = asyncio.Event() - self.send_started = asyncio.Event() - self.close_started = asyncio.Event() - self.bytes_started = asyncio.Event() - self.sent_text: list[str] = [] - self.wire_order: list[str] = [] - self.close_calls: list[tuple[int, str]] = [] - - async def send_text(self, text: str) -> None: - self.send_started.set() - if self.send_gate is not None: - await self.send_gate.wait() - if self.fail_send: - raise RuntimeError("socket send failed") - self.sent_text.append(text) - self.wire_order.append(str(json.loads(text)["kind"])) - - async def receive_text(self) -> str: - # 模拟断了 send 但客户端 receive 仍挂起的旧 socket - await self.receive_hang.wait() - raise RuntimeError("socket closed by peer") - - async def send_bytes(self, data: bytes) -> None: - self.bytes_started.set() - self.wire_order.append("bytes") - if self.bytes_gate is not None: - await self.bytes_gate.wait() - - async def send_json(self, data: object) -> None: - self.wire_order.append("reply") - - async def close(self, *, code: int, reason: str) -> None: - self.close_started.set() - self.close_calls.append((code, reason)) - if self.close_gate is not None: - await self.close_gate.wait() - - -def _register_test_device(runtime: Any, device_id: str) -> None: - device_key = ec.generate_private_key(ec.SECP256R1()) - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name=f"Device {device_id}", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - - -class _EphemeralMasterKeys: - def __init__(self) -> None: - self.keys: dict[str, bytes] = {} - - def create(self) -> tuple[str, bytes]: - key_id = uuid4().hex - key = secrets.token_bytes(32) - self.keys[key_id] = key - return key_id, key - - def load(self, master_key_id: str) -> bytes: - try: - return self.keys[master_key_id] - except KeyError as error: - raise KeyProtectionError("测试 master key 不存在") from error - - -def _config() -> MobileRealtimeConfig: - return MobileRealtimeConfig( - enabled=True, - database=Path("data/mobile.db"), - lan_hostname="akashic.local", - public_url="wss://agent.example.com/ws", - key_encryption=MobileKeyEncryptionConfig( - keyset_manifest=Path("data/mobile/keys/current.json") - ), - ) - - -@pytest.mark.asyncio -async def test_gateway_file_provider_persists_identity_across_restart( - tmp_path: Path, -) -> None: - config = MobileRealtimeConfig( - enabled=True, - database=Path("data/mobile.db"), - lan_hostname="akashic.local", - public_url="wss://agent.example.com/ws", - key_encryption=MobileKeyEncryptionConfig( - provider="file", - master_key_file=Path("data/mobile/master-keys.json"), - keyset_manifest=Path("data/mobile/keys/current.json"), - ), - ) - - first_runtime, first_keyset = build_mobile_gateway_runtime(config, tmp_path) - first_runtime.close() - second_runtime, second_keyset = build_mobile_gateway_runtime(config, tmp_path) - try: - assert second_keyset.server_fingerprint == first_keyset.server_fingerprint - assert (tmp_path / "data/mobile/master-keys.json").is_file() - finally: - second_runtime.close() - - -def _device_public_key(private_key: ec.EllipticCurvePrivateKey) -> str: - encoded = private_key.public_key().public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - return base64.b64encode(encoded).decode("ascii") - - -def _device_proof( - *, - challenge: dict[str, object], - device_id: str, - device_key: ec.EllipticCurvePrivateKey, -) -> dict[str, object]: - client_nonce = base64.urlsafe_b64encode(secrets.token_bytes(18)).decode("ascii") - signing_bytes = device_proof_signing_bytes( - server_id=str(challenge["server_id"]), - challenge_id=str(challenge["challenge_id"]), - challenge_nonce=str(challenge["nonce"]), - device_id=device_id, - client_nonce=client_nonce, - ) - signature = device_key.sign(signing_bytes, ec.ECDSA(hashes.SHA256())) - return { - "v": 1, - "kind": "control", - "type": "device.proof", - "payload": { - "challenge_id": challenge["challenge_id"], - "device_id": device_id, - "client_nonce": client_nonce, - "signature": base64.b64encode(signature).decode("ascii"), - }, - } - - -def test_authenticated_gateway_requires_resume_and_acks_durable_sync( - tmp_path: Path, -) -> None: - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, keyset = asyncio.run(build()) - server = build_mobile_gateway_server(runtime, keyset) - assert server.config.loaded is True - assert server.config.is_ssl is True - assert server.config.ssl is not None - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - stale_turn_id = "01ARZ3NDEKTSV4RRFFQ69G5FAQ" - - async def reconcile(*, device_id: str, active_turns: tuple[str, ...]) -> None: - assert active_turns == (stale_turn_id,) - await runtime.publish_event( - device_id=device_id, - event_type="turn.interrupted", - turn_id=stale_turn_id, - payload={"status": "cancelled"}, - ) - - runtime.channel.reconcile_active_turns = AsyncMock(side_effect=reconcile) - client = TestClient(create_mobile_gateway_app(runtime)) - - with client.websocket_connect("/ws") as websocket: - challenge_frame = websocket.receive_json() - assert challenge_frame["type"] == "server.challenge" - websocket.send_json( - _device_proof( - challenge=challenge_frame["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - accepted = websocket.receive_json() - epoch = accepted["connection_epoch"] - assert accepted["type"] == "auth.accepted" - - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": [stale_turn_id]}, - } - ) - terminal = websocket.receive_json() - assert terminal["type"] == "turn.interrupted" - assert terminal["turn_id"] == stale_turn_id - assert terminal["event_seq"] == 1 - synced = websocket.receive_json() - assert synced["type"] == "sync.completed" - assert synced["event_seq"] == 2 - websocket.send_json( - { - "v": 1, - "kind": "ack", - "type": "event.ack", - "connection_epoch": epoch, - "payload": {"through_event_seq": 2}, - } - ) - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "ping", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": epoch, - "payload": {}, - } - ) - reply = websocket.receive_json() - assert reply["type"] == "ping.ok" - - cursor = runtime.storage.read_cursor(device_id) - assert cursor.acknowledged_event_seq == 2 - assert ( - runtime.storage.read_durable_events( - device_id, - after_event_seq=0, - limit=10, - ) - == () - ) - runtime.close() - - -def test_resume_reset_terminal_explicitly_bridges_client_sequence_gap( - tmp_path: Path, -) -> None: - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - try: - for index in range(3): - runtime._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": f"message-{index}"}, - ) - runtime.inbox.mark_sent(device_id, through_event_seq=3) - runtime.inbox.acknowledge(device_id, through_event_seq=3) - - replay_after, replay_through, terminal = runtime._prepare_resume( - device_id=device_id, - last_ack=1, - ) - - assert (replay_after, replay_through) == (1, 1) - assert terminal.event_seq == 4 - stored = json.loads(terminal.envelope_json) - assert stored["type"] == "sync.reset_required" - assert stored["payload"]["reason"] == "client_ack_behind_server_cursor" - finally: - runtime.close() - - -def test_resume_rebases_when_authenticated_client_ack_is_ahead( - tmp_path: Path, -) -> None: - """服务端 durable DB 回退后,下一帧继续客户端序号并要求全量重建。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - try: - runtime._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": "rolled-back"}, - ) - - replay_after, replay_through, terminal = runtime._prepare_resume( - device_id=device_id, - last_ack=5, - ) - - assert (replay_after, replay_through) == (5, 5) - assert terminal.event_seq == 6 - stored = json.loads(terminal.envelope_json) - assert stored["type"] == "sync.reset_required" - assert stored["payload"]["reason"] == "client_ack_ahead_of_server_cursor" - cursor = runtime.storage.read_cursor(device_id) - assert cursor.next_event_seq == 7 - assert cursor.sent_event_seq == 5 - assert cursor.acknowledged_event_seq == 5 - assert [ - event.event_seq - for event in runtime.storage.read_durable_events( - device_id, - after_event_seq=5, - limit=10, - ) - ] == [6] - finally: - runtime.close() - - -def test_rebased_reset_survives_runtime_restart(tmp_path: Path) -> None: - """游标重定位提交后即使进程退出,重连也只能先收到已落盘 reset。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=master_keys, - ) - - master_keys = _EphemeralMasterKeys() - device_id = uuid4().hex - runtime, _ = asyncio.run(build()) - _register_test_device(runtime, device_id) - runtime._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": "rolled-back"}, - ) - _, _, reset = runtime._prepare_resume(device_id=device_id, last_ack=5) - assert reset.event_seq == 6 - runtime.close() - - restarted, _ = asyncio.run(build()) - try: - replay_after, replay_through, terminal = restarted._prepare_resume( - device_id=device_id, - last_ack=5, - ) - - assert (replay_after, replay_through) == (5, 5) - assert terminal.event_seq == 6 - assert json.loads(terminal.envelope_json)["type"] == "sync.reset_required" - assert restarted.storage.read_cursor(device_id).next_event_seq == 7 - finally: - restarted.close() - - -@pytest.mark.asyncio -async def test_rebased_reset_replays_events_written_before_reconnect( - tmp_path: Path, -) -> None: - """reset 后离线写入的事件必须随首次重连连续送达。""" - - def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=master_keys, - ) - - master_keys = _EphemeralMasterKeys() - device_id = uuid4().hex - runtime, _ = build() - _register_test_device(runtime, device_id) - runtime._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": "rolled-back"}, - ) - _, _, reset = runtime._prepare_resume(device_id=device_id, last_ack=5) - assert reset.event_seq == 6 - runtime.close() - - restarted, _ = build() - try: - offline = restarted._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": "offline-after-reset"}, - ) - assert offline.event_seq == 7 - websocket = _ControlledWebSocket() - - await restarted._resume_and_register( - cast(Any, websocket), - device_id=device_id, - connection_epoch=1, - last_ack=5, - ) - await restarted.publish_event( - event_type="message.final", - payload={"content": "live-after-resume"}, - device_id=device_id, - ) - - async def wait_for_live_event() -> None: - while len(websocket.sent_text) < 3: - await asyncio.sleep(0) - - await asyncio.wait_for(wait_for_live_event(), timeout=1) - frames = [json.loads(text) for text in websocket.sent_text] - assert [frame["event_seq"] for frame in frames] == [6, 7, 8] - assert [frame["type"] for frame in frames] == [ - "sync.reset_required", - "message.final", - "message.final", - ] - finally: - restarted.close() - - -def test_rebase_storage_rejects_ack_outside_sqlite_range(tmp_path: Path) -> None: - """存储 owner 不接受无法继续分配 reset 的客户端序号。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - try: - with pytest.raises(ValueError, match="SQLite 序号范围"): - runtime.storage.rebase_cursor_with_durable_event( - device_id, - through_event_seq=(1 << 63) - 2, - event_id=uuid4().hex, - envelope_json='{"type":"sync.reset_required"}', - created_at=datetime.now(timezone.utc), - ) - finally: - runtime.close() - - -def test_maximum_rebase_ack_can_complete_next_resume(tmp_path: Path) -> None: - """最大合法恢复 ACK 仍为 reset 确认后的完成帧保留充足序号空间。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - maximum_ack = 1 << 62 - try: - _, _, reset = runtime._prepare_resume( - device_id=device_id, - last_ack=maximum_ack, - ) - assert reset.event_seq == maximum_ack + 1 - runtime.storage.mark_events_sent( - device_id, - through_event_seq=reset.event_seq, - ) - runtime.storage.acknowledge_durable_events( - device_id, - through_event_seq=reset.event_seq, - ) - - replay_after, replay_through, completed = runtime._prepare_resume( - device_id=device_id, - last_ack=reset.event_seq, - ) - - assert (replay_after, replay_through) == (reset.event_seq, reset.event_seq) - assert completed.event_seq == maximum_ack + 2 - assert json.loads(completed.envelope_json)["type"] == "sync.completed" - finally: - runtime.close() - - -def test_ahead_ack_rebases_before_expired_inbox_check(tmp_path: Path) -> None: - """服务端回退与旧事件过期并存时,仍按客户端下一序号原子 reset。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - runtime.storage.append_durable_event( - device_id=device_id, - event_id=uuid4().hex, - envelope_json='{"type":"message.final"}', - created_at=datetime.now(timezone.utc) - timedelta(days=8), - ) - try: - replay_after, replay_through, terminal = runtime._prepare_resume( - device_id=device_id, - last_ack=5, - ) - - assert (replay_after, replay_through) == (5, 5) - assert terminal.event_seq == 6 - assert json.loads(terminal.envelope_json)["type"] == "sync.reset_required" - assert [ - event.event_seq - for event in runtime.storage.read_durable_events( - device_id, - after_event_seq=5, - limit=10, - ) - ] == [6] - finally: - runtime.close() - - -def test_resume_resets_when_durable_inbox_has_sequence_gap( - tmp_path: Path, -) -> None: - """持久化窗口缺号时直接重建,避免客户端永久重连。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - try: - for index in range(3): - runtime._enqueue_event( - device_id=device_id, - event_type="message.final", - payload={"content": f"message-{index}"}, - ) - with closing(sqlite3.connect(runtime.storage.db_path)) as db, db: - db.execute( - "DELETE FROM mobile_device_inbox WHERE device_id = ? AND event_seq = ?", - (device_id, 1), - ) - - replay_after, replay_through, terminal = runtime._prepare_resume( - device_id=device_id, - last_ack=0, - ) - - assert (replay_after, replay_through) == (0, 0) - assert terminal.event_seq == 4 - stored = json.loads(terminal.envelope_json) - assert stored["type"] == "sync.reset_required" - assert stored["payload"]["reason"] == "inbox_sequence_gap" - assert runtime.storage.count_durable_events(device_id) == 3 - - runtime.inbox.mark_sent(device_id, through_event_seq=terminal.event_seq) - runtime.inbox.acknowledge(device_id, through_event_seq=terminal.event_seq) - replay_after, replay_through, completed = runtime._prepare_resume( - device_id=device_id, - last_ack=terminal.event_seq, - ) - - assert (replay_after, replay_through) == (4, 4) - assert json.loads(completed.envelope_json)["type"] == "sync.completed" - finally: - runtime.close() - - -def test_gateway_restart_allocates_epoch_newer_than_previous_connection( - tmp_path: Path, -) -> None: - import asyncio - - master_keys = _EphemeralMasterKeys() - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=master_keys, - ) - - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime, _ = asyncio.run(build()) - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - with TestClient(create_mobile_gateway_app(runtime)).websocket_connect("/ws") as ws: - challenge = ws.receive_json() - ws.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - first_epoch = ws.receive_json()["connection_epoch"] - runtime.close() - - restarted, _ = asyncio.run(build()) - with TestClient(create_mobile_gateway_app(restarted)).websocket_connect( - "/ws" - ) as ws: - challenge = ws.receive_json() - ws.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - restarted_epoch = ws.receive_json()["connection_epoch"] - - assert restarted_epoch > first_epoch - restarted.close() - - -def test_gateway_rejects_business_frame_before_device_authentication( - tmp_path: Path, -) -> None: - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, _ = asyncio.run(build()) - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - _ = websocket.receive_json() - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "ping", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "payload": {}, - } - ) - error = websocket.receive_json() - assert error["type"] == "protocol.error" - assert error["payload"]["code"] == 4401 - runtime.close() - - -def test_authenticated_gateway_rejects_malformed_attachment_binary( - tmp_path: Path, -) -> None: - """验证坏二进制帧以明确协议错误关闭,而不是逃出 ASGI。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, _ = asyncio.run(build()) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("attachments-v1",), - ) - ) - - with TestClient(create_mobile_gateway_app(runtime)).websocket_connect("/ws") as ws: - challenge = ws.receive_json() - ws.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - epoch = ws.receive_json()["connection_epoch"] - ws.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert ws.receive_json()["type"] == "sync.completed" - ws.send_bytes(b"\x00") - error = ws.receive_json() - - assert error["type"] == "protocol.error" - assert error["payload"]["code"] == 4400 - runtime.close() - - -def test_authenticated_gateway_reports_validation_without_echoing_payload( - tmp_path: Path, -) -> None: - """验证协议字段错误可定位,但不会把用户载荷写回手机或关闭原因。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - session_id = f"akashic:{uuid4()}" - - with TestClient(create_mobile_gateway_app(runtime)).websocket_connect("/ws") as ws: - challenge = ws.receive_json() - ws.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - epoch = ws.receive_json()["connection_epoch"] - ws.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert ws.receive_json()["type"] == "sync.completed" - ws.send_json( - { - "v": 1, - "kind": "command", - "type": "message.send", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": epoch, - "session_id": session_id, - "payload": { - "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "session_id": session_id, - "text": "private-message-body", - "media_refs": [], - "client_created_at": datetime.now(timezone.utc).isoformat(), - "reply_to": "private-reference-body", - }, - } - ) - error = ws.receive_json() - with pytest.raises(WebSocketDisconnect) as closed: - ws.receive_json() - - assert error["type"] == "protocol.error" - assert error["payload"]["code"] == 4410 - assert error["payload"]["message"] == "协议字段无效" - assert "private-message-body" not in error["payload"]["message"] - assert "private-reference-body" not in error["payload"]["message"] - assert closed.value.code == 4410 - assert closed.value.reason == "协议字段无效" - runtime.close() - - -def test_offline_session_update_is_durable_and_replayed( - tmp_path: Path, -) -> None: - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, _ = asyncio.run(build()) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - second_device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=second_device_id, - public_key=_device_public_key(ec.generate_private_key(ec.SECP256R1())), - display_name="Second Pixel", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - chat_id = str(uuid4()) - session_id = f"akashic:{chat_id}" - runtime.storage.claim_session( - device_id=device_id, - session_id=session_id, - created_at=datetime.now(timezone.utc), - ) - asyncio.run( - runtime.publish_event( - event_type="session.updated", - session_id=session_id, - payload={ - "session_id": session_id, - "message_id": f"{session_id}:7", - "head_seq": 7, - }, - ) - ) - assert runtime.storage.count_durable_events(device_id) == 1 - assert runtime.storage.count_durable_events(second_device_id) == 1 - - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - challenge_frame = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge_frame["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - accepted = websocket.receive_json() - epoch = accepted["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - updated = websocket.receive_json() - synced = websocket.receive_json() - - assert updated["type"] == "session.updated" - assert updated["session_id"] == session_id - assert updated["payload"]["head_seq"] == 7 - assert synced["type"] == "sync.completed" - runtime.close() - - -def test_publish_event_respects_required_capability(tmp_path: Path) -> None: - """output.completed 只入箱声明了能力的设备,旧客户端不收到该事件。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, _ = asyncio.run(build()) - capable_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=capable_id, - public_key=_device_public_key(ec.generate_private_key(ec.SECP256R1())), - display_name="New Client", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1", TURN_OUTPUT_COMPLETED_CAPABILITY), - ) - ) - legacy_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=legacy_id, - public_key=_device_public_key(ec.generate_private_key(ec.SECP256R1())), - display_name="Legacy Client", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - asyncio.run( - runtime.publish_event( - event_type="turn.output.completed", - payload={"client_message_id": "cmid-1"}, - session_id="akashic:abc", - turn_id="turn-1", - required_capability=TURN_OUTPUT_COMPLETED_CAPABILITY, - ) - ) - assert runtime.storage.count_durable_events(capable_id) == 1 - assert runtime.storage.count_durable_events(legacy_id) == 0 - runtime.close() - - -def test_device_update_refreshes_capabilities_and_unlocks_event( - tmp_path: Path, -) -> None: - """已配对旧客户端升级后 device.update 刷新能力,无需重新配对即可收到新事件。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - import asyncio - - runtime, _ = asyncio.run(build()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(ec.generate_private_key(ec.SECP256R1())), - display_name="Upgraded Client", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - # 升级前:旧能力收不到 output.completed - asyncio.run( - runtime.publish_event( - event_type="turn.output.completed", - payload={"client_message_id": "cmid-0"}, - session_id="akashic:abc", - turn_id="turn-0", - required_capability=TURN_OUTPUT_COMPLETED_CAPABILITY, - ) - ) - assert runtime.storage.count_durable_events(device_id) == 0 - - # device.update 刷新能力声明 - asyncio.run( - runtime.refresh_device_capabilities( - device_id=device_id, - capabilities=("stream-v1", TURN_OUTPUT_COMPLETED_CAPABILITY), - ) - ) - assert runtime.storage.read_device(device_id).capabilities == ( - "stream-v1", - TURN_OUTPUT_COMPLETED_CAPABILITY, - ) - - # 升级后:新能力收到 output.completed - asyncio.run( - runtime.publish_event( - event_type="turn.output.completed", - payload={"client_message_id": "cmid-1"}, - session_id="akashic:abc", - turn_id="turn-1", - required_capability=TURN_OUTPUT_COMPLETED_CAPABILITY, - ) - ) - assert runtime.storage.count_durable_events(device_id) == 1 - runtime.close() - - -def test_authenticated_message_send_reaches_agent_event_path_once( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """验证 WSS command 进入 InboundMessage,并按顺序返回完整事件流。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - class LoopbackBus: - def __init__(self) -> None: - self.inbound: list[RawInbound] = [] - self.legacy_publish_calls = 0 - - async def publish_inbound(self, message: object) -> None: - self.legacy_publish_calls += 1 - raise AssertionError("Mobile v3 fixture 不得调用 legacy publish_inbound") - - async def reserve_mobile_channel_handoff(self, raw: RawInbound) -> bool: - assert isinstance(raw, RawInbound) - assert raw.message.metadata["mobile_v3_handoff"] is True - return True - - async def defer_mobile_channel_handoff(self, handoff_id: str) -> None: - raise AssertionError(f"unexpected deferred Mobile v3 handoff: {handoff_id}") - - def has_pending_mobile_handoff( - self, - *, - session_key: str, - client_message_id: str, - ) -> bool: - return False - - async def admit(self, raw: RawInbound) -> bool: - assert isinstance(raw, RawInbound) - inbound = raw.message - assert isinstance(inbound, ChannelInboundMessage) - self.inbound.append(raw) - turn_id = uuid4().hex - await runtime.channel._on_turn_started( - TurnStarted( - session_key=cast(str, inbound.metadata["session_key_override"]), - channel=inbound.channel, - chat_id=inbound.chat_id, - content=inbound.content, - timestamp=inbound.timestamp, - turn_id=turn_id, - ) - ) - await runtime.channel._on_stream_delta( - StreamDeltaReady( - session_key=cast(str, inbound.metadata["session_key_override"]), - channel=inbound.channel, - chat_id=inbound.chat_id, - turn_id=turn_id, - thinking_delta="先检查", - ) - ) - await runtime.channel._on_tool_call_started( - ToolCallStarted( - session_key=cast(str, inbound.metadata["session_key_override"]), - channel=inbound.channel, - chat_id=inbound.chat_id, - iteration=1, - call_id="call-1", - tool_name="shell", - arguments={"command": "pwd"}, - turn_id=turn_id, - ) - ) - await runtime.channel._on_tool_call_completed( - ToolCallCompleted( - session_key=cast(str, inbound.metadata["session_key_override"]), - channel=inbound.channel, - chat_id=inbound.chat_id, - iteration=1, - call_id="call-1", - tool_name="shell", - arguments={"command": "pwd"}, - final_arguments={"command": "pwd"}, - status="success", - result_preview="ok", - turn_id=turn_id, - ) - ) - await runtime.channel._on_stream_delta( - StreamDeltaReady( - session_key=cast(str, inbound.metadata["session_key_override"]), - channel=inbound.channel, - chat_id=inbound.chat_id, - turn_id=turn_id, - thinking_delta="工具后继续", - ) - ) - receipt = await runtime.channel._deliver_message( - channel_message_from_outbound( - OutboundMessage( - channel="akashic", - chat_id=inbound.chat_id, - content="完成", - thinking="先检查", - control_turn_id=turn_id, - execution_attempt_id=turn_id, - metadata={"_channel_commit_role": "passive"}, - ) - ) - ) - assert receipt.succeeded - return True - - class FakeEventBus: - def on(self, event_type: type[object], callback: object) -> None: - return None - - class FakePushTool: - pass - - import asyncio - - runtime, _ = asyncio.run(build()) - bus = LoopbackBus() - asyncio.run( - runtime.channel.start( - cast( - Any, - SimpleNamespace( - bus=bus, - session_manager=SessionManager(tmp_path / "sessions"), - event_bus=FakeEventBus(), - push_tool=FakePushTool(), - interrupt_controller=None, - attachment_store=AttachmentStore(tmp_path / "uploads"), - ), - ) - ) - ) - adapter = asyncio.run( - _attach_open_mobile_v3( - runtime.channel, - bus, - binding_token="gateway-event-fixture", - ) - ) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - session_id = f"akashic:{uuid4()}" - command_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV" - command = { - "v": 1, - "kind": "command", - "type": "message.send", - "id": command_id, - "connection_epoch": 1, - "session_id": session_id, - "payload": { - "client_message_id": command_id, - "session_id": session_id, - "text": "帮我检查", - "media_refs": [], - "client_created_at": datetime.now(timezone.utc).isoformat(), - }, - } - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - challenge_frame = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge_frame["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - accepted = websocket.receive_json() - epoch = accepted["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert websocket.receive_json()["type"] == "sync.completed" - command["connection_epoch"] = epoch - websocket.send_json(command) - frames = [websocket.receive_json() for _ in range(7)] - assert [frame["type"] for frame in frames] == [ - "turn.started", - "react.thinking.delta", - "react.tool.started", - "react.tool.completed", - "react.thinking.delta", - "message.final", - "message.send.ok", - ] - first_thinking, tool_started, tool_completed, second_thinking = ( - frames[1], - frames[2], - frames[3], - frames[4], - ) - assert first_thinking["payload"]["ordinal"] == 0 - assert tool_started["payload"]["ordinal"] == 1 - assert tool_completed["payload"]["ordinal"] == 1 - assert ( - tool_completed["payload"]["block_id"] == tool_started["payload"]["block_id"] - ) - assert second_thinking["payload"]["ordinal"] == 2 - - websocket.send_json(command) - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "ping", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAX", - "connection_epoch": epoch, - "payload": {}, - } - ) - assert websocket.receive_json()["type"] == "message.send.ok" - assert websocket.receive_json()["type"] == "ping.ok" - - assert len(bus.inbound) == 1 - assert bus.legacy_publish_calls == 0 - reply_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:send.reply_sent" - and record.akashic_fields.get("client_message_id") == command_id - ] - assert len(reply_records) == 2 - assert [record.akashic_fields["outcome"] for record in reply_records] == [ - "sent", - "receipt_replayed", - ] - assert [record.akashic_fields["receipt_replayed"] for record in reply_records] == [ - False, - True, - ] - assert all( - record.akashic_fields["device_id"] == device_id for record in reply_records - ) - assert all( - record.akashic_fields["connection_epoch"] == epoch for record in reply_records - ) - assert all( - record.akashic_fields["reply_type"] == "message.send.ok" - for record in reply_records - ) - asyncio.run(adapter.stop()) - asyncio.run(runtime.channel.stop()) - runtime.close() - - -def test_plugin_ui_failure_keeps_authenticated_websocket_available( - tmp_path: Path, -) -> None: - """插件面板失败后,同一连接仍能继续处理命令。""" - - class FailedMobileUiProvider: - def catalog(self) -> dict[str, object]: - return {"catalog_revision": "a" * 64, "items": []} - - async def query(self, *args: object, **kwargs: object) -> dict[str, object]: - raise MobileUiRpcExecutionError( - "插件 mobile UI RPC 执行失败: fitbit@mobile-lab.fitbit.overview" - ) - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - runtime.channel.bind_mobile_ui_provider(cast(Any, FailedMobileUiProvider())) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel7", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - - # 1. 完成认证,并在活动连接上触发插件失败 - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - challenge = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - accepted = websocket.receive_json() - epoch = accepted["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert websocket.receive_json()["type"] == "sync.completed" - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "plugin.ui.query", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FB5", - "connection_epoch": epoch, - "payload": { - "owner_id": "dashboard:fitbit", - "plugin_id": "fitbit@mobile-lab", - "plugin_revision": "revision-1", - "method": "fitbit.overview", - "payload": {}, - "slot": "dashboard.main", - }, - } - ) - failed = websocket.receive_json() - assert failed["type"] == "plugin.ui.query.error" - assert failed["payload"]["code"] == "plugin_failed" - - # 2. 错误回复不能改变 epoch,也不能阻断后续命令 - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "ping", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FB6", - "connection_epoch": epoch, - "payload": {}, - } - ) - assert websocket.receive_json()["type"] == "ping.ok" - - runtime.close() - - -def test_plugin_ui_https_data_plane_uses_signed_request_bound_ticket( - tmp_path: Path, -) -> None: - query_calls: list[dict[str, object]] = [] - - class MobileUiProvider: - def catalog(self) -> dict[str, object]: - return {"catalog_revision": "a" * 64, "items": []} - - async def query( - self, - plugin_id: str, - plugin_revision: str, - method: str, - payload: dict[str, object], - *, - session_id: str | None, - turn_id: str | None, - ) -> dict[str, object]: - query_calls.append(payload) - return { - "schema": "akasha.recall-card.v1", - "plugin_id": plugin_id, - "plugin_revision": plugin_revision, - "method": method, - "payload": payload, - "session_id": session_id, - "turn_id": turn_id, - } - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build()) - runtime.channel.bind_mobile_ui_provider(cast(Any, MobileUiProvider())) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel7", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - command_id = "01ARZ3NDEKTSV4RRFFQ69G5FB7" - query_payload = { - "owner_id": "turn:akasha", - "plugin_id": "akasha@builtin", - "plugin_revision": "revision-1", - "method": "recall.current", - "payload": {"message_id": "message:446"}, - "slot": "turn.before_reasoning", - } - request_body = { - "request_id": command_id, - **query_payload, - "session_id": None, - "turn_id": None, - } - - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - challenge = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - accepted = websocket.receive_json() - epoch = accepted["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert websocket.receive_json()["type"] == "sync.completed" - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "plugin.ui.query.prepare", - "id": command_id, - "connection_epoch": epoch, - "payload": query_payload, - } - ) - ready = websocket.receive_json() - assert ready["type"] == "plugin.ui.query.ready", ready - assert len(json.dumps(ready).encode("utf-8")) < 2 * 1024 - - response = client.post( - ready["payload"]["path"], - headers={ - "Authorization": f"Bearer {ready['payload']['ticket']}", - }, - json=request_body, - ) - assert response.status_code == 200 - assert response.headers["cache-control"] == "no-store" - assert query_calls == [{"message_id": "message:446"}] - assert response.json() == { - "schema": "akasha.recall-card.v1", - "plugin_id": "akasha@builtin", - "plugin_revision": "revision-1", - "method": "recall.current", - "payload": {"message_id": "message:446"}, - "session_id": None, - "turn_id": None, - } - - tampered = client.post( - ready["payload"]["path"], - headers={ - "Authorization": f"Bearer {ready['payload']['ticket']}", - }, - json={**request_body, "turn_id": "turn-other"}, - ) - assert tampered.status_code == 401 - assert tampered.json()["code"] == "invalid_ticket" - assert len(query_calls) == 1 - - disconnected = client.post( - ready["payload"]["path"], - headers={ - "Authorization": f"Bearer {ready['payload']['ticket']}", - }, - json=request_body, - ) - assert disconnected.status_code == 401 - assert disconnected.json()["code"] == "invalid_ticket" - assert len(query_calls) == 1 - - runtime.close() - - -def test_plugin_ui_https_ticket_expires_before_query_execution( - tmp_path: Path, -) -> None: - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, keyset = asyncio.run(build()) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - current = [datetime(2026, 7, 28, tzinfo=timezone.utc)] - issuer = PluginUiHttpTicketIssuer( - keyset, - runtime.storage, - clock=lambda: current[0], - ) - body: dict[str, object] = { - "request_id": "01ARZ3NDEKTSV4RRFFQ69G5FB8", - "owner_id": "owner", - } - grant = issuer.issue( - device_id=device_id, - connection_epoch=1, - request_body=body, - ) - - runtime.storage.revoke_device( - device_id, - revoked_at=current[0], - ) - with pytest.raises(PluginUiHttpTicketError, match="设备无效"): - issuer.verify(grant.ticket, request_body=body) - - current[0] += timedelta(seconds=31) - - with pytest.raises(PluginUiHttpTicketError, match="已过期"): - issuer.verify(grant.ticket, request_body=body) - - runtime.close() - - -def test_attachment_upload_resumes_and_reaches_agent_media( - tmp_path: Path, - request: pytest.FixtureRequest, -) -> None: - """验证二进制上传跨连接续传,并以 media_refs 进入 Agent。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - class CaptureBus: - def __init__(self) -> None: - self.inbound: list[RawInbound] = [] - self.legacy_publish_calls = 0 - - async def publish_inbound(self, message: object) -> None: - self.legacy_publish_calls += 1 - raise AssertionError("Mobile v3 fixture 不得调用 legacy publish_inbound") - - async def reserve_mobile_channel_handoff(self, raw: RawInbound) -> bool: - assert isinstance(raw, RawInbound) - assert raw.message.metadata["mobile_v3_handoff"] is True - return True - - async def defer_mobile_channel_handoff(self, handoff_id: str) -> None: - raise AssertionError(f"unexpected deferred Mobile v3 handoff: {handoff_id}") - - def has_pending_mobile_handoff( - self, - *, - session_key: str, - client_message_id: str, - ) -> bool: - return False - - async def admit(self, raw: RawInbound) -> bool: - assert isinstance(raw, RawInbound) - assert isinstance(raw.message, ChannelInboundMessage) - self.inbound.append(raw) - return True - - class FakeEventBus: - def on(self, event_type: type[object], callback: object) -> None: - return None - - class FakePushTool: - pass - - import asyncio - - runtime, _ = asyncio.run(build()) - request.addfinalizer(runtime.close) - bus = CaptureBus() - session_manager = SessionManager(tmp_path / "sessions") - runtime.channel.bind_channel_attachment_store( - ChannelAttachmentArtifactStore( - workspace=session_manager.workspace, - session_store=session_manager.control_store, - ) - ) - asyncio.run( - runtime.channel.start( - cast( - Any, - SimpleNamespace( - bus=bus, - session_manager=session_manager, - event_bus=FakeEventBus(), - push_tool=FakePushTool(), - interrupt_controller=None, - attachment_store=AttachmentStore(tmp_path / "uploads"), - ), - ) - ) - ) - adapter = asyncio.run( - _attach_open_mobile_v3( - runtime.channel, - bus, - binding_token="gateway-attachment-fixture", - ) - ) - request.addfinalizer(lambda: asyncio.run(runtime.channel.stop())) - request.addfinalizer(lambda: asyncio.run(adapter.stop())) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1", "attachments-v1"), - ) - ) - session_id = f"akashic:{uuid4()}" - attachment_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV" - confirmed_offset = 1024 * 1024 - content = b"a" * confirmed_offset + b"resumed payload" - digest = hashlib.sha256(content).hexdigest() - client = TestClient(create_mobile_gateway_app(runtime)) - - with client.websocket_connect("/ws") as websocket: - challenge = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - epoch = websocket.receive_json()["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - synced = websocket.receive_json() - assert synced["type"] == "sync.completed" - websocket.send_json( - { - "v": 1, - "kind": "ack", - "type": "event.ack", - "connection_epoch": epoch, - "payload": {"through_event_seq": synced["event_seq"]}, - } - ) - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "attachment.begin", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", - "connection_epoch": epoch, - "session_id": session_id, - "payload": { - "attachment_id": attachment_id, - "filename": "meme.png", - "content_type": "image/png", - "size_bytes": len(content), - "sha256": digest, - }, - } - ) - begin = websocket.receive_json() - assert begin["type"] == "attachment.begin.ok" - assert begin["payload"]["next_offset"] == 0 - for offset in range(0, confirmed_offset, 128 * 1024): - websocket.send_bytes( - encode_attachment_chunk( - AttachmentChunk( - attachment_id, - offset, - content[offset : offset + 128 * 1024], - ) - ) - ) - confirmed = websocket.receive_json() - assert confirmed["type"] == "attachment.progress" - assert confirmed["payload"]["transferred_bytes"] == confirmed_offset - - with client.websocket_connect("/ws") as websocket: - challenge = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - epoch = websocket.receive_json()["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": { - "last_ack": confirmed["event_seq"], - "active_turns": [], - }, - } - ) - assert websocket.receive_json()["type"] == "sync.completed" - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "attachment.begin", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAX", - "connection_epoch": epoch, - "session_id": session_id, - "payload": { - "attachment_id": attachment_id, - "filename": "meme.png", - "content_type": "image/png", - "size_bytes": len(content), - "sha256": digest, - }, - } - ) - resumed = websocket.receive_json() - assert resumed["payload"]["next_offset"] == confirmed_offset - websocket.send_bytes( - encode_attachment_chunk( - AttachmentChunk( - attachment_id, - confirmed_offset, - content[confirmed_offset:], - ) - ) - ) - progress = websocket.receive_json() - assert progress["type"] == "attachment.progress" - assert progress["payload"]["transferred_bytes"] == len(content) - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "attachment.finish", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAY", - "connection_epoch": epoch, - "session_id": session_id, - "payload": {"attachment_id": attachment_id}, - } - ) - assert websocket.receive_json()["type"] == "attachment.ready" - assert websocket.receive_json()["type"] == "attachment.finish.ok" - websocket.send_json( - { - "v": 1, - "kind": "command", - "type": "message.send", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAZ", - "connection_epoch": epoch, - "session_id": session_id, - "payload": { - "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAZ", - "session_id": session_id, - "text": "", - "media_refs": [attachment_id], - "client_created_at": datetime.now(timezone.utc).isoformat(), - }, - } - ) - assert websocket.receive_json()["type"] == "message.send.ok" - - assert len(bus.inbound) == 1 - assert bus.legacy_publish_calls == 0 - raw = bus.inbound[0] - assert raw.message.content == "" - artifact_ids = cast(tuple[str, ...], raw.message.metadata["attachment_ids"]) - assert len(artifact_ids) == 1 - assert tuple(ref.artifact_id for ref in raw.message.attachments) == artifact_ids - artifact = session_manager.control_store.get_attachment(artifact_ids[0]) - assert artifact is not None - assert artifact.state == "ready" - assert artifact.size_bytes == len(content) - assert (session_manager.workspace / artifact.storage_key).read_bytes() == content - - -def test_outbound_attachment_download_replays_binary_before_reply( - tmp_path: Path, - request: pytest.FixtureRequest, -) -> None: - """验证出站附件只暴露描述符,并以可重复 offset 下载二进制。""" - - async def build(): - return build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - - class CapturePushTool: - pass - - import asyncio - - runtime, _ = asyncio.run(build()) - request.addfinalizer(runtime.close) - push = CapturePushTool() - asyncio.run( - runtime.channel.start( - cast( - Any, - SimpleNamespace( - bus=SimpleNamespace(), - session_manager=SessionManager(tmp_path / "sessions"), - event_bus=SimpleNamespace(on=lambda *_: None), - push_tool=push, - interrupt_controller=None, - attachment_store=AttachmentStore(tmp_path / "uploads"), - ), - ) - ) - ) - request.addfinalizer(lambda: asyncio.run(runtime.channel.stop())) - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_device_public_key(device_key), - display_name="Pixel Emulator", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("attachments-v1",), - ) - ) - chat_id = str(uuid4()) - session_id = f"akashic:{chat_id}" - runtime.storage.claim_session( - device_id=device_id, - session_id=session_id, - created_at=datetime.now(timezone.utc), - ) - content = b"outbound" * 20_000 - source = tmp_path / "agent-result.bin" - source.write_bytes(content) - turn_id = uuid4().hex - persisted = runtime.channel._require_ctx().session_manager.get_or_create(session_id) - persisted.add_message( - "assistant", - "文件已生成", - media=[str(source)], - id=uuid4().hex, - ) - runtime.channel._require_ctx().session_manager.save(persisted) - asyncio.run( - runtime.channel._on_turn_started( - TurnStarted( - session_key=session_id, - channel="akashic", - chat_id=chat_id, - content="生成文件", - timestamp=datetime.now(timezone.utc), - turn_id=turn_id, - ) - ) - ) - asyncio.run( - runtime.channel._deliver_message( - channel_message_from_outbound( - OutboundMessage( - channel="akashic", - chat_id=chat_id, - content="文件已生成", - media=[str(source)], - control_turn_id=turn_id, - execution_attempt_id=turn_id, - session_message_id=str(persisted.messages[-1]["id"]), - metadata={"_channel_commit_role": "passive"}, - ) - ) - ) - ) - - client = TestClient(create_mobile_gateway_app(runtime)) - with client.websocket_connect("/ws") as websocket: - challenge = websocket.receive_json() - websocket.send_json( - _device_proof( - challenge=challenge["payload"], - device_id=device_id, - device_key=device_key, - ) - ) - epoch = websocket.receive_json()["connection_epoch"] - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": 0, "active_turns": []}, - } - ) - assert websocket.receive_json()["type"] == "turn.started" - final = websocket.receive_json() - assert final["type"] == "message.final" - descriptor = final["payload"]["attachments"][0] - assert "local_path" not in descriptor - assert websocket.receive_json()["type"] == "sync.completed" - - command = { - "v": 1, - "kind": "command", - "type": "attachment.download", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": epoch, - "session_id": session_id, - "payload": { - "attachment_id": descriptor["attachment_id"], - "offset": 0, - }, - } - websocket.send_json(command) - first = decode_attachment_chunk(websocket.receive_bytes()) - reply = websocket.receive_json() - assert first.data == content[:MAX_ATTACHMENT_CHUNK_BYTES] - assert reply["type"] == "attachment.download.ok" - assert reply["payload"]["next_offset"] == len(first.data) - - websocket.send_json(command) - duplicate = decode_attachment_chunk(websocket.receive_bytes()) - assert duplicate == first - assert websocket.receive_json() == reply - - -def test_slow_device_delivery_does_not_block_other_device(tmp_path: Path) -> None: - """验证慢设备只阻塞自身队列,其他设备仍能实时收到事件。""" - - async def scenario() -> None: - # 1. 注册两个在线设备,其中一个 socket 人为阻塞 - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - slow_id = uuid4().hex - fast_id = uuid4().hex - _register_test_device(runtime, slow_id) - _register_test_device(runtime, fast_id) - slow_gate = asyncio.Event() - slow_socket = _ControlledWebSocket(send_gate=slow_gate) - fast_socket = _ControlledWebSocket() - slow_connection = ActiveMobileConnection( - websocket=cast(Any, slow_socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - fast_connection = ActiveMobileConnection( - websocket=cast(Any, fast_socket), - connection_epoch=2, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - runtime._connections[slow_id] = slow_connection - runtime._connections[fast_id] = fast_connection - - # 2. fanout 返回后,快设备应在慢设备解除阻塞前完成写入 - await runtime.publish_event( - event_type="connection.degraded", - payload={"reason": "fanout-test"}, - ) - await asyncio.wait_for(slow_socket.send_started.wait(), timeout=1) - await asyncio.wait_for(fast_socket.send_started.wait(), timeout=1) - assert len(fast_socket.sent_text) == 1 - assert slow_socket.sent_text == [] - - # 3. 解除慢设备后,其 durable 序号仍按顺序推进 - slow_task = slow_connection.delivery_task - assert slow_task is not None - slow_gate.set() - await asyncio.wait_for(slow_task, timeout=1) - assert len(slow_socket.sent_text) == 1 - assert runtime.storage.read_cursor(slow_id).sent_event_seq == 1 - assert runtime.storage.read_cursor(fast_id).sent_event_seq == 1 - runtime.close() - - asyncio.run(scenario()) - - -def test_live_drain_sends_plain_and_terminal_events_with_identity( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """验证在线排空普通与终态事件:均写入 socket、cursor 推进、身份日志不崩。""" - - async def scenario() -> str: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - # 1. 普通事件经真实 _drain_connection 写入并推进 sent cursor - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "plain-live"}, - ) - plain_task = runtime._connections[device_id].delivery_task - assert plain_task is not None - await asyncio.wait_for(plain_task, timeout=5) - assert len(socket.sent_text) == 1 - assert runtime.storage.read_cursor(device_id).sent_event_seq == 1 - - # 2. 终态事件带 session/turn,identity 观测路径正常执行 - await runtime.publish_event( - device_id=device_id, - event_type="message.final", - payload={"content": "done"}, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - terminal_task = runtime._connections[device_id].delivery_task - assert terminal_task is not None - await asyncio.wait_for(terminal_task, timeout=5) - frames = [json.loads(text) for text in socket.sent_text] - assert [frame["type"] for frame in frames] == [ - "connection.degraded", - "message.final", - ] - assert runtime.storage.read_cursor(device_id).sent_event_seq == 2 - runtime.close() - return device_id - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - registered_device_id = asyncio.run(scenario()) - sent_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.sent" - ] - assert len(sent_records) == 1 - assert sent_records[0].akashic_fields["session_id"] == "akashic:s1" - assert sent_records[0].akashic_fields["turn_id"] == "akashic:t1" - assert sent_records[0].akashic_fields["client_message_id"] == "" - assert sent_records[0].akashic_fields["counts"] == ( - f"event_type=message.final device_id={registered_device_id} " - f"event_seq=2 connection_epoch=1" - ) - queued_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.queued" - ] - assert len(queued_records) == 1 - assert queued_records[0].akashic_fields["session_id"] == "akashic:s1" - assert queued_records[0].akashic_fields["turn_id"] == "akashic:t1" - assert queued_records[0].akashic_fields["counts"] == ( - f"event_type=message.final device_id={registered_device_id} event_seq=2" - ) - - -def test_broken_socket_send_failure_closes_socket_and_resume_replays_once( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """验证 send 失败摘除连接后主动 close 旧 socket、cursor 不推进、不记 sent, - resume 恰好重放一次终态事件,并在新 epoch 记一次 sent。""" - - async def scenario() -> str: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - broken_socket = _ControlledWebSocket(fail_send=True) - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, broken_socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - # 1. 在线投递终态事件时 send_text 失败(客户端 receive 仍挂起) - await runtime.publish_event( - device_id=device_id, - event_type="message.final", - payload={ - "content": "done", - "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - }, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - await asyncio.wait_for(broken_socket.send_started.wait(), timeout=5) - - # 2. 等待 drain 摘除当前连接、并主动 close 旧 socket,cursor 未推进 - for _ in range(500): - if device_id not in runtime._connections: - break - await asyncio.sleep(0.01) - assert device_id not in runtime._connections - await asyncio.wait_for(broken_socket.close_started.wait(), timeout=5) - assert broken_socket.close_calls == [ - (4408, "连接投递失败,请重新连接恢复"), - ] - assert broken_socket.sent_text == [] - cursor = runtime.storage.read_cursor(device_id) - assert cursor.sent_event_seq == 0 - assert cursor.next_event_seq == 2 - - # 3. 新连接 resume 后同一 durable 终态事件恰好重放一次并推进 cursor - new_socket = _ControlledWebSocket() - await runtime._resume_and_register( - cast(Any, new_socket), - device_id=device_id, - connection_epoch=2, - last_ack=0, - ) - frames = [json.loads(text) for text in new_socket.sent_text] - assert [frame["type"] for frame in frames] == [ - "message.final", - "sync.completed", - ] - assert [frame["event_seq"] for frame in frames] == [1, 2] - assert runtime.storage.read_cursor(device_id).sent_event_seq == 2 - runtime.close() - return device_id - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - registered_device_id = asyncio.run(scenario()) - sent_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.sent" - ] - # 失败 epoch 绝不记 sent;新 epoch 重放同 seq 成功只记一次。 - assert len(sent_records) == 1 - assert sent_records[0].akashic_fields["session_id"] == "akashic:s1" - assert sent_records[0].akashic_fields["turn_id"] == "akashic:t1" - assert ( - sent_records[0].akashic_fields["client_message_id"] - == "01ARZ3NDEKTSV4RRFFQ69G5FAV" - ) - assert sent_records[0].akashic_fields["counts"] == ( - f"event_type=message.final device_id={registered_device_id} " - f"event_seq=1 connection_epoch=2" - ) - - -def test_replaced_epoch_during_send_records_no_sent_and_replay_records_once( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """旧连接 send_text 阻塞期间被新 epoch 替换:旧 owner 未推进 cursor 绝不记 - sent,新 epoch resume 重放同 seq 后只记一条 sent。""" - - async def scenario() -> str: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - old_socket = _ControlledWebSocket(send_gate=asyncio.Event()) - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, old_socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - # 1. 在线投递终态事件:旧 epoch 的 drain 阻塞在真实 send_text 写锁内 - await runtime.publish_event( - device_id=device_id, - event_type="message.final", - payload={ - "content": "done", - "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", - }, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - await asyncio.wait_for(old_socket.send_started.wait(), timeout=5) - old_task = runtime._connections[device_id].delivery_task - assert old_task is not None - - # 2. 阻塞期间新 epoch 经真实 _resume_and_register 替换旧连接 - new_socket = _ControlledWebSocket() - await runtime._resume_and_register( - cast(Any, new_socket), - device_id=device_id, - connection_epoch=2, - last_ack=0, - ) - assert runtime._connections[device_id].connection_epoch == 2 - - # 3. 释放旧 gate:旧 send 物理完成但 cursor 不推进、不记 sent - old_socket.send_gate.set() - await asyncio.wait_for(old_task, timeout=5) - cursor = runtime.storage.read_cursor(device_id) - assert cursor.sent_event_seq == 2 - - # 4. 新 epoch 重放同 seq 的 message.final 与 sync.completed - frames = [json.loads(text) for text in new_socket.sent_text] - finals = [frame for frame in frames if frame["type"] == "message.final"] - assert [frame["event_seq"] for frame in finals] == [1] - runtime.close() - return device_id - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - registered_device_id = asyncio.run(scenario()) - sent_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.sent" - ] - # 旧 epoch 不记 sent;新 epoch 重放同 seq 只记一条。 - assert len(sent_records) == 1 - assert sent_records[0].akashic_fields["session_id"] == "akashic:s1" - assert sent_records[0].akashic_fields["turn_id"] == "akashic:t1" - assert ( - sent_records[0].akashic_fields["client_message_id"] - == "01ARZ3NDEKTSV4RRFFQ69G5FAW" - ) - assert sent_records[0].akashic_fields["counts"] == ( - f"event_type=message.final device_id={registered_device_id} " - f"event_seq=1 connection_epoch=2" - ) - - -def test_terminal_queued_records_zero_device_without_false_report( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """无活动设备时终态事件不入任何 inbox,queued 绝不虚报。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - await runtime.publish_event( - event_type="message.final", - payload={"content": "done"}, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - assert runtime.storage.list_active_devices() == () - runtime.close() - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - asyncio.run(scenario()) - queued_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.queued" - ] - assert queued_records == [] - - -def test_terminal_queued_records_one_milestone_per_device( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """终态事件按 enqueue_many 真实返回的设备副本逐设备记录 queued。""" - - async def scenario() -> tuple[str, str]: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_a = uuid4().hex - device_b = uuid4().hex - _register_test_device(runtime, device_a) - _register_test_device(runtime, device_b) - await runtime.publish_event( - event_type="turn.interrupted", - payload={"status": "interrupted", "reason": "test"}, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - runtime.close() - return device_a, device_b - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - device_a, device_b = asyncio.run(scenario()) - queued_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.queued" - ] - assert len(queued_records) == 2 - assert {record.akashic_fields["counts"] for record in queued_records} == { - f"event_type=turn.interrupted device_id={device_a} event_seq=1", - f"event_type=turn.interrupted device_id={device_b} event_seq=1", - } - for record in queued_records: - assert record.akashic_fields["session_id"] == "akashic:s1" - assert record.akashic_fields["turn_id"] == "akashic:t1" - - -def test_terminal_without_identity_still_advances_cursor_and_delivery_task( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -) -> None: - """缺 session/turn/client 身份时 sent 观测缺省为 missing,不破坏投递任务与 cursor。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - await runtime.publish_event( - device_id=device_id, - event_type="message.final", - payload={"content": "done"}, - ) - terminal_task = runtime._connections[device_id].delivery_task - assert terminal_task is not None - await asyncio.wait_for(terminal_task, timeout=5) - assert runtime.storage.read_cursor(device_id).sent_event_seq == 1 - runtime.close() - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - asyncio.run(scenario()) - sent_records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.sent" - ] - assert len(sent_records) == 1 - assert sent_records[0].akashic_fields["session_id"] == "" - assert sent_records[0].akashic_fields["turn_id"] == "" - assert sent_records[0].akashic_fields["client_message_id"] == "" - - -def test_terminal_milestone_logger_contract_is_no_throw( - caplog: pytest.LogCaptureFixture, -) -> None: - """观测使用的 turn_milestone 契约 no-throw:真实 logger 下相同字段形状不抛错, - 且结构化字段完整落入记录。""" - - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - logger = logging.getLogger("infra.mobile_realtime.gateway") - gateway_module.turn_milestone( - logger, - "tl:event.sent", - session_id="akashic:s1", - turn_id="akashic:t1", - client_message_id="cmid-t", - counts=( - "event_type=message.final device_id=d1 " "event_seq=2 connection_epoch=3" - ), - ) - records = [ - record - for record in caplog.records - if getattr(record, "akashic_fields", {}).get("event") == "tl:event.sent" - ] - assert len(records) == 1 - assert records[0].akashic_fields["session_id"] == "akashic:s1" - assert records[0].akashic_fields["turn_id"] == "akashic:t1" - assert records[0].akashic_fields["client_message_id"] == "cmid-t" - assert records[0].akashic_fields["counts"] == ( - "event_type=message.final device_id=d1 event_seq=2 connection_epoch=3" - ) - - -def test_live_observation_failure_keeps_cursor_and_epoch_after_send( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """sent 观测抛错时:frame 已送、cursor 已推进、连接/epoch 保持、同 seq 不重发。""" - - def broken_milestone(*_args: object, **_kwargs: object) -> None: - raise RuntimeError("milestone logger broken") - - monkeypatch.setattr(gateway_module, "turn_milestone", broken_milestone) - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - # 1. 终态事件真实 send;观测抛错不得回滚 cursor、摘除连接或杀死任务 - await runtime.publish_event( - device_id=device_id, - event_type="message.final", - payload={"content": "done", "client_message_id": "cmid-t"}, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - terminal_task = runtime._connections[device_id].delivery_task - assert terminal_task is not None - await asyncio.wait_for(terminal_task, timeout=5) - assert [json.loads(text)["type"] for text in socket.sent_text] == [ - "message.final" - ] - assert [json.loads(text)["event_seq"] for text in socket.sent_text] == [1] - cursor = runtime.storage.read_cursor(device_id) - assert cursor.sent_event_seq == 1 - assert device_id in runtime._connections - assert runtime._connections[device_id].connection_epoch == 1 - - # 2. 同一 epoch 的后续投递照常工作:cursor 继续推进,同 seq 绝不重发 - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "after-failure"}, - ) - second_task = runtime._connections[device_id].delivery_task - assert second_task is not None - await asyncio.wait_for(second_task, timeout=5) - frames = [json.loads(text) for text in socket.sent_text] - assert [frame["event_seq"] for frame in frames] == [1, 2] - assert len(frames) == len({frame["event_seq"] for frame in frames}) - assert runtime.storage.read_cursor(device_id).sent_event_seq == 2 - - # 3. 客户端按已推进 cursor resume:无任何旧 seq 重放 - await runtime._resume_and_register( - cast(Any, socket), - device_id=device_id, - connection_epoch=3, - last_ack=2, - ) - replay_frames = [json.loads(text) for text in socket.sent_text] - assert [frame["type"] for frame in replay_frames] == [ - "message.final", - "connection.degraded", - "sync.completed", - ] - assert all(frame["event_seq"] <= 3 for frame in replay_frames) - assert [frame["event_seq"] for frame in replay_frames].count(1) == 1 - assert [frame["event_seq"] for frame in replay_frames].count(2) == 1 - runtime.close() - - asyncio.run(scenario()) - failure_records = [ - record for record in caplog.records if "sent 观测失败" in record.getMessage() - ] - assert len(failure_records) == 1 - assert "event_seq=1" in failure_records[0].getMessage() - - -def test_resume_observation_failure_keeps_cursor_without_duplicate( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """resume 重放终态后观测抛错:帧已送、cursor 已推进、再次 resume 不重放同 seq。""" - - def broken_milestone(*_args: object, **_kwargs: object) -> None: - raise RuntimeError("milestone logger broken") - - monkeypatch.setattr(gateway_module, "turn_milestone", broken_milestone) - caplog.set_level(logging.INFO, logger="infra.mobile_realtime.gateway") - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - await runtime.publish_event( - event_type="message.final", - payload={"content": "done", "client_message_id": "cmid-t"}, - session_id="akashic:s1", - turn_id="akashic:t1", - ) - - # 1. 无在线连接时事件只入箱;resume 重放终态 + sync.completed - socket = _ControlledWebSocket() - await runtime._resume_and_register( - cast(Any, socket), - device_id=device_id, - connection_epoch=2, - last_ack=0, - ) - frames = [json.loads(text) for text in socket.sent_text] - assert [frame["type"] for frame in frames] == [ - "message.final", - "sync.completed", - ] - assert [frame["event_seq"] for frame in frames] == [1, 2] - cursor = runtime.storage.read_cursor(device_id) - assert cursor.sent_event_seq == 2 - assert device_id in runtime._connections - assert runtime._connections[device_id].connection_epoch == 2 - - # 2. 观测虽失败但 cursor 已提交:按 sent cursor 再次 resume 不重放同 seq - second_socket = _ControlledWebSocket() - await runtime._resume_and_register( - cast(Any, second_socket), - device_id=device_id, - connection_epoch=3, - last_ack=2, - ) - replay_frames = [json.loads(text) for text in second_socket.sent_text] - assert [frame["type"] for frame in replay_frames] == ["sync.completed"] - assert all(frame["event_seq"] != 1 for frame in replay_frames) - assert all(frame["event_seq"] != 2 for frame in replay_frames) - runtime.close() - - asyncio.run(scenario()) - failure_records = [ - record for record in caplog.records if "sent 观测失败" in record.getMessage() - ] - assert len(failure_records) == 1 - assert "event_seq=1" in failure_records[0].getMessage() - - -def test_connection_control_only_reaches_matching_current_connection( - tmp_path: Path, -) -> None: - """验证临时控制帧不入箱,且只投递给匹配的当前 epoch。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=2, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - await runtime.publish_connection_control( - device_id=device_id, - connection_epoch=1, - control_type="plugin.ui.changed", - payload={}, - ) - - assert runtime.storage.read_cursor(device_id).next_event_seq == 1 - assert socket.sent_text == [] - - await runtime.publish_connection_control( - device_id=device_id, - connection_epoch=2, - control_type="plugin.ui.changed", - payload={}, - ) - - assert runtime.storage.read_cursor(device_id).next_event_seq == 1 - assert json.loads(socket.sent_text[0]) == { - "connection_epoch": 2, - "kind": "control", - "payload": {}, - "type": "plugin.ui.changed", - "v": 1, - } - runtime.close() - - asyncio.run(scenario()) - - -def test_connection_control_timeout_only_removes_slow_connection( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """验证控制帧写超时不会卡住调用方或污染 durable cursor。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket(send_gate=asyncio.Event()) - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - monkeypatch.setattr( - gateway_module, - "_CONNECTION_CONTROL_SEND_TIMEOUT_SECONDS", - 0.01, - ) - - await runtime.publish_connection_control( - device_id=device_id, - connection_epoch=1, - control_type="plugin.ui.changed", - payload={}, - ) - - assert device_id not in runtime._connections - await asyncio.wait_for(socket.close_started.wait(), timeout=1) - assert socket.close_calls == [ - (4408, "连接控制帧投递失败,请重新连接恢复"), - ] - assert runtime.storage.read_cursor(device_id).next_event_seq == 1 - runtime.close() - - asyncio.run(scenario()) - - -def test_connection_control_waits_for_inflight_frame_without_removing_connection( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """验证正常在途帧占锁超过控制帧超时也不会被误判为慢连接。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - send_lock = asyncio.Lock() - await send_lock.acquire() - connection = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=send_lock, - pending_events=deque(), - ready=True, - delivery_task=None, - ) - runtime._connections[device_id] = connection - monkeypatch.setattr( - gateway_module, - "_CONNECTION_CONTROL_SEND_TIMEOUT_SECONDS", - 0.01, - ) - - delivery = asyncio.create_task( - runtime.publish_connection_control( - device_id=device_id, - connection_epoch=1, - control_type="plugin.ui.changed", - payload={}, - ) - ) - await asyncio.sleep(0.03) - - assert not delivery.done() - assert runtime._connections[device_id] is connection - assert socket.close_calls == [] - - send_lock.release() - await asyncio.wait_for(delivery, timeout=1) - assert runtime._connections[device_id] is connection - assert len(socket.sent_text) == 1 - runtime.close() - - asyncio.run(scenario()) - - -def test_connection_control_lock_timeout_removes_stalled_connection( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """验证长期持锁的失活连接不会永久阻塞插件目录刷新。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - socket = _ControlledWebSocket() - send_lock = asyncio.Lock() - await send_lock.acquire() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, socket), - connection_epoch=1, - send_lock=send_lock, - pending_events=deque(), - ready=True, - delivery_task=None, - ) - monkeypatch.setattr( - gateway_module, - "_CONNECTION_CONTROL_LOCK_TIMEOUT_SECONDS", - 0.01, - ) - - await runtime.publish_connection_control( - device_id=device_id, - connection_epoch=1, - control_type="plugin.ui.changed", - payload={}, - ) - - assert device_id not in runtime._connections - assert socket.sent_text == [] - await asyncio.wait_for(socket.close_started.wait(), timeout=1) - assert socket.close_calls == [ - (4408, "连接控制帧投递失败,请重新连接恢复"), - ] - assert send_lock.locked() - send_lock.release() - runtime.close() - - asyncio.run(scenario()) - - -def test_resume_window_queues_concurrent_event_after_sync_terminal( - tmp_path: Path, -) -> None: - """验证 resume 阻塞期间发布的事件不会漏发、重发或越过终止帧。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "before-resume"}, - ) - send_gate = asyncio.Event() - websocket = _ControlledWebSocket(send_gate=send_gate) - - # 1. 卡住历史事件写入,并在 resume 窗口发布实时事件 - resume_task = asyncio.create_task( - runtime._resume_and_register( - cast(Any, websocket), - device_id=device_id, - connection_epoch=1, - last_ack=0, - ) - ) - await asyncio.wait_for(websocket.send_started.wait(), timeout=1) - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "during-resume"}, - ) - send_gate.set() - await asyncio.wait_for(resume_task, timeout=1) - - # 2. 等待独立投递任务排空重放期间的实时事件 - connection = runtime._connections[device_id] - delivery_task = connection.delivery_task - if delivery_task is not None: - await asyncio.wait_for(delivery_task, timeout=1) - frames = [json.loads(text) for text in websocket.sent_text] - assert [frame["type"] for frame in frames] == [ - "connection.degraded", - "sync.completed", - "connection.degraded", - ] - assert frames[0]["payload"]["reason"] == "before-resume" - assert frames[2]["payload"]["reason"] == "during-resume" - assert [frame["event_seq"] for frame in frames] == [1, 2, 3] - assert runtime.storage.read_cursor(device_id).sent_event_seq == 3 - runtime.close() - - asyncio.run(scenario()) - - -def test_replaced_socket_close_does_not_block_other_device( - tmp_path: Path, -) -> None: - """验证旧连接关闭卡住时,不会占用其他设备的投递路径。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - replaced_id = uuid4().hex - other_id = uuid4().hex - _register_test_device(runtime, replaced_id) - _register_test_device(runtime, other_id) - close_gate = asyncio.Event() - old_socket = _ControlledWebSocket(close_gate=close_gate) - runtime._connections[replaced_id] = ActiveMobileConnection( - websocket=cast(Any, old_socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - other_socket = _ControlledWebSocket() - runtime._connections[other_id] = ActiveMobileConnection( - websocket=cast(Any, other_socket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - - # 1. 新连接完成 resume,但旧 socket 的 close 持续阻塞 - new_socket = _ControlledWebSocket() - await runtime._resume_and_register( - cast(Any, new_socket), - device_id=replaced_id, - connection_epoch=2, - last_ack=0, - ) - await asyncio.wait_for(old_socket.close_started.wait(), timeout=1) - - # 2. 另一设备仍能收到实时事件 - await runtime.publish_event( - device_id=other_id, - event_type="connection.degraded", - payload={"reason": "other-device"}, - ) - await asyncio.wait_for(other_socket.send_started.wait(), timeout=1) - assert len(other_socket.sent_text) == 1 - close_gate.set() - await asyncio.sleep(0) - runtime.close() - - asyncio.run(scenario()) - - -def test_binary_reply_is_atomic_against_event_delivery(tmp_path: Path) -> None: - """验证下载二进制与 reply 之间不会插入实时事件。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - bytes_gate = asyncio.Event() - websocket = _ControlledWebSocket(bytes_gate=bytes_gate) - connection = ActiveMobileConnection( - websocket=cast(Any, websocket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - runtime._connections[device_id] = connection - - class BinaryReplyChannel: - async def handle_command(self, **_: object) -> object: - return SimpleNamespace( - binary=AttachmentChunk("01ARZ3NDEKTSV4RRFFQ69G5FAV", 0, b"data"), - type="attachment.download.ok", - payload={"next_offset": 4}, - session_id=None, - turn_id=None, - ) - - runtime._channel = cast(Any, BinaryReplyChannel()) - frame = parse_frame( - json.dumps( - { - "v": 1, - "kind": "command", - "type": "attachment.download", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", - "connection_epoch": 1, - "session_id": f"akashic:{uuid4()}", - "payload": { - "attachment_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "offset": 0, - }, - } - ) - ) - assert isinstance(frame, AttachmentDownloadCommand) - - # 1. 二进制写入持锁期间发布事件,事件任务只能等待 - command_task = asyncio.create_task( - runtime._handle_command( - cast(Any, websocket), - frame, - connection_epoch=1, - device_id=device_id, - ) - ) - await asyncio.wait_for(websocket.bytes_started.wait(), timeout=1) - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "atomic-order"}, - ) - await asyncio.sleep(0) - assert websocket.wire_order == ["bytes"] - - # 2. 解锁后必须先 reply,再发送排队事件 - bytes_gate.set() - await asyncio.wait_for(command_task, timeout=1) - for _ in range(20): - if len(websocket.wire_order) == 3: - break - await asyncio.sleep(0) - assert websocket.wire_order == ["bytes", "reply", "event"] - runtime.close() - - asyncio.run(scenario()) - - -def test_slow_connection_queue_overflow_keeps_durable_events( - tmp_path: Path, -) -> None: - """验证实时队列超限会断开慢连接,但 durable inbox 完整保留。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - websocket = _ControlledWebSocket() - connection = ActiveMobileConnection( - websocket=cast(Any, websocket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=False, - delivery_task=None, - ) - runtime._connections[device_id] = connection - - # 1. resume 尚未 ready 时持续排队,第 65 个事件触发慢消费者降级 - for index in range(65): - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": f"queued-{index}"}, - ) - await asyncio.wait_for(websocket.close_started.wait(), timeout=1) - assert device_id not in runtime._connections - - # 2. 网络队列被丢弃,但 65 个事件仍可从 durable inbox 恢复 - durable = runtime.storage.read_durable_events( - device_id, - after_event_seq=0, - limit=100, - ) - assert len(durable) == 65 - assert [event.event_seq for event in durable] == list(range(1, 66)) - assert runtime.storage.read_cursor(device_id).sent_event_seq == 0 - runtime.close() - - asyncio.run(scenario()) - - -def test_command_reply_stops_at_causal_event_barrier(tmp_path: Path) -> None: - """验证后续高频事件不会让当前命令 reply 等到整队列排空。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - send_gate = asyncio.Event() - websocket = _ControlledWebSocket(send_gate=send_gate) - connection = ActiveMobileConnection( - websocket=cast(Any, websocket), - connection_epoch=1, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=True, - delivery_task=None, - ) - runtime._connections[device_id] = connection - - class CausalEventChannel: - async def handle_command(self, **_: object) -> object: - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "causal"}, - ) - return SimpleNamespace( - binary=None, - type="session.list.ok", - payload={"sessions": []}, - session_id=None, - turn_id=None, - ) - - runtime._channel = cast(Any, CausalEventChannel()) - frame = parse_frame( - json.dumps( - { - "v": 1, - "kind": "command", - "type": "session.list", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAX", - "connection_epoch": 1, - "payload": {}, - } - ) - ) - - # 1. 命令因果事件卡在 socket,同时追加一批后续事件 - command_task = asyncio.create_task( - runtime._handle_command( - cast(Any, websocket), - cast(Any, frame), - connection_epoch=1, - device_id=device_id, - ) - ) - await asyncio.wait_for(websocket.send_started.wait(), timeout=1) - for index in range(20): - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": f"later-{index}"}, - ) - - # 2. 放行后 reply 紧跟因果事件,不等待后续 20 个事件排空 - send_gate.set() - await asyncio.wait_for(command_task, timeout=1) - assert websocket.wire_order[:2] == ["event", "reply"] - for _ in range(100): - if len(websocket.wire_order) == 22: - break - await asyncio.sleep(0) - assert websocket.wire_order == ["event", "reply"] + ["event"] * 20 - runtime.close() - - asyncio.run(scenario()) - - -def test_resume_pages_only_to_frozen_high_watermark(tmp_path: Path) -> None: - """验证大于单页的 backlog 分页重放,并在冻结上限后发送 terminal。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - for index in range(600): - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": f"backlog-{index}"}, - ) - websocket = _ControlledWebSocket() - - await runtime._resume_and_register( - cast(Any, websocket), - device_id=device_id, - connection_epoch=1, - last_ack=0, - ) - frames = [json.loads(text) for text in websocket.sent_text] - assert len(frames) == 601 - assert frames[-1]["type"] == "sync.completed" - assert frames[-1]["payload"]["replayed_events"] == 600 - assert [frame["event_seq"] for frame in frames] == list(range(1, 602)) - runtime.close() - - asyncio.run(scenario()) - - -def test_replaced_connection_ack_cannot_delete_new_resume_window( - tmp_path: Path, -) -> None: - """验证旧 epoch 的迟到 ACK 不能删除新连接需要重放的 durable 前缀。""" - - async def scenario() -> None: - runtime, _ = build_mobile_gateway_runtime( - _config(), - tmp_path, - master_keys=_EphemeralMasterKeys(), - ) - device_id = uuid4().hex - _register_test_device(runtime, device_id) - await runtime.publish_event( - device_id=device_id, - event_type="connection.degraded", - payload={"reason": "must-replay"}, - ) - _ = runtime.inbox.mark_sent(device_id, through_event_seq=1) - old_socket = _ControlledWebSocket() - new_socket = _ControlledWebSocket() - runtime._connections[device_id] = ActiveMobileConnection( - websocket=cast(Any, new_socket), - connection_epoch=2, - send_lock=asyncio.Lock(), - pending_events=deque(), - ready=False, - delivery_task=None, - ) - - # 1. 新连接已占住活动代际后,旧连接 ACK 必须被拒绝 - acknowledged = await runtime._acknowledge_active_connection( - device_id=device_id, - websocket=cast(Any, old_socket), - connection_epoch=1, - through_event_seq=1, - ) - assert acknowledged is False - assert runtime.storage.read_cursor(device_id).acknowledged_event_seq == 0 - assert ( - len( - runtime.storage.read_durable_events( - device_id, - after_event_seq=0, - limit=10, - ) - ) - == 1 - ) - - # 2. 只有当前 websocket 与 epoch 的 ACK 可以删除该前缀 - acknowledged = await runtime._acknowledge_active_connection( - device_id=device_id, - websocket=cast(Any, new_socket), - connection_epoch=2, - through_event_seq=1, - ) - assert acknowledged is True - assert runtime.storage.read_cursor(device_id).acknowledged_event_seq == 1 - assert ( - runtime.storage.read_durable_events( - device_id, - after_event_seq=0, - limit=10, - ) - == () - ) - runtime.close() - - asyncio.run(scenario()) diff --git a/tests/mobile_realtime/test_inbox.py b/tests/mobile_realtime/test_inbox.py deleted file mode 100644 index c5e8bdfa0..000000000 --- a/tests/mobile_realtime/test_inbox.py +++ /dev/null @@ -1,177 +0,0 @@ -from __future__ import annotations - -import json -from datetime import datetime, timedelta, timezone -from pathlib import Path - -import pytest - -from infra.mobile_realtime.inbox import DurableInboxManager, InboxResetRequired -from infra.mobile_realtime.storage import ( - DeviceRecord, - MobileRealtimeStorage, - UnknownDeviceError, -) - - -NOW = datetime(2026, 7, 14, 9, 30, tzinfo=timezone.utc) - - -def _build_storage(tmp_path: Path) -> MobileRealtimeStorage: - storage = MobileRealtimeStorage(tmp_path / "mobile.db") - storage.register_device( - DeviceRecord( - device_id="device-1", - public_key="public-key", - display_name="Phone", - created_at=NOW, - revoked_at=None, - capabilities=("chat",), - ) - ) - return storage - - -def _envelope(event_id: str) -> str: - return json.dumps( - { - "v": 1, - "kind": "event", - "type": "message.final", - "id": event_id, - "payload": {}, - } - ) - - -def test_inbox_replays_in_sequence_and_acknowledges_p0(tmp_path: Path) -> None: - storage = _build_storage(tmp_path) - try: - manager = DurableInboxManager(storage, clock=lambda: NOW) - first = manager.enqueue( - device_id="device-1", - event_id="event-1", - envelope_json=_envelope("event-1"), - ) - second = manager.enqueue( - device_id="device-1", - event_id="event-2", - envelope_json=_envelope("event-2"), - ) - - replay = manager.replay("device-1", after_event_seq=0, limit=10) - assert replay.events == (first, second) - assert replay.cursor.next_event_seq == 3 - - manager.mark_sent("device-1", through_event_seq=2) - ack = manager.acknowledge("device-1", through_event_seq=2) - assert ack.deleted_events == 2 - assert storage.count_durable_events("device-1") == 0 - finally: - storage.close() - - -def test_inbox_enqueues_broadcast_in_one_atomic_write(tmp_path: Path) -> None: - storage = _build_storage(tmp_path) - storage.register_device( - DeviceRecord( - device_id="device-2", - public_key="public-key-2", - display_name="Tablet", - created_at=NOW, - revoked_at=None, - capabilities=("chat",), - ) - ) - try: - manager = DurableInboxManager(storage, clock=lambda: NOW) - events = manager.enqueue_many( - device_ids=("device-1", "device-2"), - event_id="event-shared", - envelope_json=_envelope("event-shared"), - ) - - assert [event.device_id for event in events] == ["device-1", "device-2"] - assert [event.event_seq for event in events] == [1, 1] - assert events[0].created_at == events[1].created_at == NOW - assert storage.read_cursor("device-1").next_event_seq == 2 - assert storage.read_cursor("device-2").next_event_seq == 2 - finally: - storage.close() - - -def test_inbox_broadcast_rolls_back_all_devices_when_one_is_unknown(tmp_path: Path) -> None: - storage = _build_storage(tmp_path) - try: - manager = DurableInboxManager(storage, clock=lambda: NOW) - - with pytest.raises(UnknownDeviceError, match="设备不存在或缺少 cursor"): - manager.enqueue_many( - device_ids=("device-1", "missing-device"), - event_id="event-shared", - envelope_json=_envelope("event-shared"), - ) - - assert storage.count_durable_events("device-1") == 0 - assert storage.read_cursor("device-1").next_event_seq == 1 - finally: - storage.close() - - -def test_expired_p0_requires_reset_without_silent_deletion(tmp_path: Path) -> None: - storage = _build_storage(tmp_path) - try: - current_time = NOW - manager = DurableInboxManager(storage, clock=lambda: current_time) - manager.enqueue( - device_id="device-1", - event_id="event-1", - envelope_json=_envelope("event-1"), - ) - current_time = NOW + timedelta(days=8) - - with pytest.raises(InboxResetRequired, match="恢复窗口"): - manager.replay("device-1", after_event_seq=0, limit=10) - assert storage.count_durable_events("device-1") == 1 - finally: - storage.close() - - -def test_default_retention_allows_exactly_seven_days_then_requires_reset( - tmp_path: Path, -) -> None: - storage = _build_storage(tmp_path) - try: - current_time = NOW - manager = DurableInboxManager(storage, clock=lambda: current_time) - manager.enqueue( - device_id="device-1", - event_id="event-1", - envelope_json=_envelope("event-1"), - ) - - current_time = NOW + timedelta(days=7) - assert len(manager.replay("device-1", after_event_seq=0, limit=10).events) == 1 - - current_time += timedelta(microseconds=1) - with pytest.raises(InboxResetRequired, match="恢复窗口"): - manager.replay("device-1", after_event_seq=0, limit=10) - finally: - storage.close() - - -def test_inbox_rejects_naive_clock(tmp_path: Path) -> None: - storage = _build_storage(tmp_path) - try: - manager = DurableInboxManager( - storage, - clock=lambda: datetime(2026, 7, 14, 9, 30), - ) - with pytest.raises(ValueError, match="带时区"): - manager.enqueue( - device_id="device-1", - event_id="event-1", - envelope_json=_envelope("event-1"), - ) - finally: - storage.close() diff --git a/tests/mobile_realtime/test_isolated_e2e.py b/tests/mobile_realtime/test_isolated_e2e.py deleted file mode 100644 index 1f2b13871..000000000 --- a/tests/mobile_realtime/test_isolated_e2e.py +++ /dev/null @@ -1,1094 +0,0 @@ -from __future__ import annotations - -import asyncio -import base64 -import hashlib -import re -import secrets -import threading -from contextlib import suppress -from datetime import datetime, timezone -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from uuid import uuid4 - -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec -from fastapi.testclient import TestClient - -from agent.config_models import MobileKeyEncryptionConfig, MobileRealtimeConfig -from agent.control.ports import ControlExecutionResult -from agent.control.scoped_turn import TurnAcceptedReceipt -from agent.control.runtime import ConversationRuntime -from agent.plugin_composition.channels import ( - ChannelCommitRole, - ChannelDeliveryReceipt, - ChannelFactoryContext, - ChannelInboundMessage, - ChannelRuntimePorts, - JsonValue, - ProviderDeliveryRequest, - RawInbound, -) -from agent.plugin_composition.durable_deliveries import ( - DurableBindingAttempt, - DurableDeliveryRequest, - PluginDurableDeliveries, -) -from agent.plugin_composition.durable_delivery_store import DurableDeliveryStore -from bootstrap.chat_api import create_chat_app -from bootstrap.core_channel_adapter import build_core_channel_definition -from bootstrap.passive_worker import PassiveMessageWorker -from bootstrap.tools import _dispatch_v3_durable_delivery -from bus.event_bus import EventBus -from bus.events import OutboundMessage, channel_message_from_outbound -from bus.events_lifecycle import StreamDeltaReady, TurnOutputCompleted, TurnStarted -from bus.queue import MessageBus -from infra.channels.base import AttachmentStore -from infra.channels.akashic_channel import AkashicChannel -from infra.channels.web_chat_channel import WebChatChannel -from infra.mobile_realtime.attachments import decode_attachment_chunk -from infra.mobile_realtime.auth import device_proof_signing_bytes -from infra.mobile_realtime.gateway import ( - MobileGatewayRuntime, - build_mobile_gateway_runtime, - create_mobile_gateway_app, -) -from infra.mobile_realtime.key_protection import KeyProtectionError -from infra.mobile_realtime.storage import DeviceRecord -from agent.plugins.manager import PluginManager -from session.manager import SessionManager - - -async def _attach_open_mobile_v3( - channel: Any, - ingress: Any, - *, - binding_token: str, -) -> Any: - """Attach one exact v3 ingress and open admission for this fixture.""" - - context = ChannelFactoryContext( - snapshot_id="isolated-e2e-snapshot", - generation_id="isolated-e2e-generation", - binding_token=binding_token, - config={}, - credentials={}, - provider_client_factory=cast(Any, object()), - ingress=ingress, - identity=None, - ) - adapter = channel.build_v3_adapter(context) - adapter.attach_runtime( - ChannelRuntimePorts( - snapshot_id=context.snapshot_id, - generation_id=context.generation_id, - binding_token=context.binding_token, - ingress=context.ingress, - identity=context.identity, - attachment_import=context.attachment_import, - ) - ) - assert (await adapter.start()).binding_token == binding_token - adapter.open_admission() - return adapter - - -class _EphemeralMasterKeys: - def __init__(self) -> None: - self.keys: dict[str, bytes] = {} - - def create(self) -> tuple[str, bytes]: - key_id = uuid4().hex - key = secrets.token_bytes(32) - self.keys[key_id] = key - return key_id, key - - def load(self, master_key_id: str) -> bytes: - try: - return self.keys[master_key_id] - except KeyError as error: - raise KeyProtectionError("隔离测试 master key 不存在") from error - - -class _EventBus: - def on(self, event_type: type[object], callback: object) -> None: - return None - - -class _PushTool: - pass - - -class _SharedSessionBus: - """Persist public Web/Mobile ingress into one real SessionManager.""" - - def __init__(self, manager: SessionManager, event_bus: EventBus) -> None: - self._manager = manager - self._event_bus = event_bus - self._adapter: Any | None = None - self._recovery = None - self._count = 0 - self._changed = threading.Condition() - - def bind_adapter(self, adapter: Any) -> None: - self._adapter = adapter - - def bind_mobile_channel_inbound_recoverer(self, callback: object) -> None: - self._recovery = callback - - async def reserve_mobile_channel_handoff(self, raw: RawInbound) -> bool: - assert raw.message.metadata.get("mobile_v3_handoff") is True - return True - - async def defer_mobile_channel_handoff(self, handoff_id: str) -> None: - raise AssertionError(f"unexpected deferred handoff: {handoff_id}") - - def has_pending_mobile_handoff( - self, - *, - session_key: str, - client_message_id: str, - ) -> bool: - return False - - async def admit(self, raw: RawInbound) -> bool: - session_id = str( - raw.message.metadata.get("session_key_override") - or f"akashic:{raw.message.chat_id}" - ) - session = self._manager.get_or_create(session_id) - session.add_message( - "user", - raw.message.content, - client_message_id=raw.message_id, - ) - self._manager.save(session) - turn_id = f"turn:shared:{self._count + 1}" - await self._event_bus.fanout( - TurnStarted( - session_key=session_id, - channel="akashic", - chat_id=raw.message.chat_id, - content=raw.message.content, - timestamp=datetime.now(timezone.utc), - turn_id=turn_id, - control_turn_id=turn_id, - client_message_id=raw.message_id, - ) - ) - await self._event_bus.fanout( - StreamDeltaReady( - session_key=session_id, - channel="akashic", - chat_id=raw.message.chat_id, - turn_id=turn_id, - thinking_delta="共享思考", - content_delta="共享回答", - ) - ) - await self._event_bus.fanout( - TurnOutputCompleted( - session_key=session_id, - channel="akashic", - chat_id=raw.message.chat_id, - turn_id=turn_id, - client_message_id=raw.message_id, - ) - ) - if self._adapter is None: - raise RuntimeError("Shared Akashic fixture 尚未绑定 adapter") - receipt = await self._adapter.deliver( - ProviderDeliveryRequest( - binding_token="shared-e2e-binding", - delivery_id=f"reply:{raw.message_id}", - recipient=raw.message.chat_id, - body="共享回答", - thinking="共享思考", - metadata={"client_message_id": raw.message_id}, - commit_role=ChannelCommitRole.PASSIVE, - control_turn_id=turn_id, - execution_attempt_id=turn_id, - ) - ) - assert receipt.status.value == "delivered" - with self._changed: - self._count += 1 - self._changed.notify_all() - return True - - def wait_for_count(self, expected: int) -> None: - with self._changed: - assert self._changed.wait_for( - lambda: self._count >= expected, - timeout=3, - ) - - -class _DeterministicAgentBus: - """把手机入站消息持久化,并返回一条带固定媒体的确定性回复。""" - - def __init__(self, manager: SessionManager, reply_media: Path) -> None: - self._manager = manager - self._reply_media = reply_media - self._runtime: MobileGatewayRuntime | None = None - self.inbound_count = 0 - self.legacy_publish_calls = 0 - - def bind(self, runtime: MobileGatewayRuntime) -> None: - self._runtime = runtime - - async def publish_inbound(self, message: object) -> None: - self.legacy_publish_calls += 1 - raise AssertionError("Mobile v3 fixture 不得调用 legacy publish_inbound") - - async def reserve_mobile_channel_handoff(self, raw: RawInbound) -> bool: - assert isinstance(raw, RawInbound) - assert raw.message.metadata["mobile_v3_handoff"] is True - return True - - async def defer_mobile_channel_handoff(self, handoff_id: str) -> None: - raise AssertionError(f"unexpected deferred Mobile v3 handoff: {handoff_id}") - - def has_pending_mobile_handoff( - self, - *, - session_key: str, - client_message_id: str, - ) -> bool: - return False - - async def admit(self, raw: RawInbound) -> bool: - """按真实持久化顺序生成 turn.started 与 message.final。""" - - # 1. 持久化同一个 client_message_id,模拟生命周期入库结果 - assert isinstance(raw, RawInbound) - inbound = raw.message - assert isinstance(inbound, ChannelInboundMessage) - runtime = self._require_runtime() - session_id = cast(str, inbound.metadata["session_key_override"]) - session = self._manager.get_or_create(session_id) - client_message_id = cast(str, inbound.metadata["client_message_id"]) - session.add_message( - "user", - inbound.content, - client_message_id=client_message_id, - ) - turn_id = uuid4().hex - session.add_message( - "assistant", - "隔离网关固定回复", - media=[str(self._reply_media)], - ) - self._manager.save(session) - assistant_message_id = str(session.messages[-1]["id"]) - self.inbound_count += 1 - - # 2. 通过真实移动渠道发布可恢复事件 - await runtime.channel._on_turn_started( - TurnStarted( - session_key=session_id, - channel="akashic", - chat_id=inbound.chat_id, - content=inbound.content, - timestamp=datetime.now(timezone.utc), - turn_id=turn_id, - control_turn_id=turn_id, - client_message_id=client_message_id, - ) - ) - receipt = await runtime.channel._deliver_message( - channel_message_from_outbound( - OutboundMessage( - channel="akashic", - chat_id=inbound.chat_id, - content="隔离网关固定回复", - media=[str(self._reply_media)], - control_turn_id=turn_id, - execution_attempt_id=turn_id, - session_message_id=assistant_message_id, - metadata={"_channel_commit_role": "passive"}, - ) - ) - ) - assert receipt.succeeded - return True - - def _require_runtime(self) -> MobileGatewayRuntime: - if self._runtime is None: - raise RuntimeError("隔离 Agent bus 尚未绑定 gateway runtime") - return self._runtime - - -def _config(root: Path) -> MobileRealtimeConfig: - return MobileRealtimeConfig( - enabled=True, - database=root / "gateway" / "mobile.db", - lan_hostname="isolated-mobile.test", - public_url="", - key_encryption=MobileKeyEncryptionConfig( - keyset_manifest=root / "gateway" / "keys" / "current.json" - ), - ) - - -def _public_key(private_key: ec.EllipticCurvePrivateKey) -> str: - encoded = private_key.public_key().public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - return base64.b64encode(encoded).decode("ascii") - - -def _proof( - challenge: dict[str, object], - device_id: str, - device_key: ec.EllipticCurvePrivateKey, -) -> dict[str, object]: - client_nonce = base64.urlsafe_b64encode(secrets.token_bytes(18)).decode("ascii") - signing_bytes = device_proof_signing_bytes( - server_id=str(challenge["server_id"]), - challenge_id=str(challenge["challenge_id"]), - challenge_nonce=str(challenge["nonce"]), - device_id=device_id, - client_nonce=client_nonce, - ) - signature = device_key.sign(signing_bytes, ec.ECDSA(hashes.SHA256())) - return { - "v": 1, - "kind": "control", - "type": "device.proof", - "payload": { - "challenge_id": challenge["challenge_id"], - "device_id": device_id, - "client_nonce": client_nonce, - "signature": base64.b64encode(signature).decode("ascii"), - }, - } - - -def _authenticate( - websocket: Any, - device_id: str, - device_key: ec.EllipticCurvePrivateKey, -) -> int: - challenge = websocket.receive_json() - assert challenge["type"] == "server.challenge" - websocket.send_json(_proof(challenge["payload"], device_id, device_key)) - accepted = websocket.receive_json() - assert accepted["type"] == "auth.accepted" - return int(accepted["connection_epoch"]) - - -def _resume(websocket: Any, epoch: int, last_ack: int) -> list[dict[str, Any]]: - websocket.send_json( - { - "v": 1, - "kind": "control", - "type": "resume", - "connection_epoch": epoch, - "payload": {"last_ack": last_ack, "active_turns": []}, - } - ) - frames: list[dict[str, Any]] = [] - while True: - frame = websocket.receive_json() - frames.append(frame) - if frame["type"] == "sync.completed": - return frames - - -def _command( - command_id: str, - command_type: str, - epoch: int, - *, - session_id: str | None = None, - payload: dict[str, object] | None = None, -) -> dict[str, object]: - frame: dict[str, object] = { - "v": 1, - "kind": "command", - "type": command_type, - "id": command_id, - "connection_epoch": epoch, - "payload": payload or {}, - } - if session_id is not None: - frame["session_id"] = session_id - return frame - - -def _history_identity(item: dict[str, Any]) -> str: - if item["role"] == "user" and item.get("client_message_id"): - return f"user:{item['client_message_id']}" - return f"{item['role']}:{item['id']}" - - -def test_web_and_mobile_share_one_session_and_receive_one_delivery( - tmp_path: Path, -) -> None: - """Exercise both public protocols against one Session projection fixture.""" - - root = tmp_path / "shared-akashic-e2e" - manager = SessionManager(root / "workspace") - - async def build_runtime() -> tuple[MobileGatewayRuntime, object]: - return build_mobile_gateway_runtime( - _config(root), - root, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build_runtime()) - web = WebChatChannel() - channel = AkashicChannel(web, runtime.channel) - event_bus = EventBus() - bus = _SharedSessionBus(manager, event_bus) - context = cast( - Any, - SimpleNamespace( - bus=bus, - session_manager=manager, - event_bus=event_bus, - push_tool=_PushTool(), - interrupt_controller=None, - attachment_store=AttachmentStore(root / "attachments"), - ), - ) - asyncio.run(channel.start(context)) - adapter = asyncio.run( - _attach_open_mobile_v3( - channel, - bus, - binding_token="shared-e2e-binding", - ) - ) - bus.bind_adapter(adapter) - - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_public_key(device_key), - display_name="Shared Akashic Harness", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1",), - ) - ) - app = create_chat_app(workspace=root, channel=web) - app.mount("/mobile", create_mobile_gateway_app(runtime)) - - try: - with TestClient(app) as client: - with ( - client.websocket_connect("/ws") as web_socket, - client.websocket_connect("/ws") as observing_web_socket, - client.websocket_connect("/mobile/ws") as mobile_socket, - ): - epoch = _authenticate(mobile_socket, device_id, device_key) - assert [ - frame["type"] for frame in _resume(mobile_socket, epoch, 0) - ] == ["sync.completed"] - - # 1. Web allocates the identity and writes through the shared ingress. - web_socket.send_json( - {"type": "session.create", "request_id": "web-create"} - ) - created = web_socket.receive_json() - session_id = cast(str, created["session_id"]) - assert re.fullmatch(r"akashic:[0-9a-f]{32}", session_id) - web_socket.send_json( - { - "type": "message.send", - "request_id": "01945f4c-2000-7000-8000-000000000001", - "session_id": session_id, - "text": "Web 写入同一个会话", - "media": [], - } - ) - bus.wait_for_count(1) - web_live = [web_socket.receive_json() for _ in range(5)] - mobile_live = [mobile_socket.receive_json() for _ in range(4)] - assert [frame["type"] for frame in web_live] == [ - "turn.started", - "react.thinking.delta", - "answer.delta", - "turn.output.completed", - "message.final", - ] - assert [frame["type"] for frame in mobile_live] == [ - "turn.started", - "react.thinking.delta", - "answer.delta", - "message.final", - ] - assert web_live[0]["client_message_id"] == ( - "01945f4c-2000-7000-8000-000000000001" - ) - - # 2. Mobile lists and reads the exact same durable Session. - mobile_socket.send_json( - _command("01J00000000000000000000020", "session.list", epoch) - ) - listed = mobile_socket.receive_json() - assert listed["type"] == "session.list" - assert mobile_socket.receive_json()["type"] == "session.list.ok" - assert [item["session_id"] for item in listed["payload"]["items"]] == [ - session_id - ] - mobile_socket.send_json( - _command( - "01J00000000000000000000021", - "history.get", - epoch, - session_id=session_id, - payload={"page": 1, "page_size": 50}, - ) - ) - first_page = mobile_socket.receive_json() - assert mobile_socket.receive_json()["type"] == "history.get.ok" - assert [item["content"] for item in first_page["payload"]["items"]] == [ - "Web 写入同一个会话" - ] - - # 3. Mobile writes back; Web HTTP history sees both messages once. - observing_web_socket.send_json({ - "type": "session.attach", - "request_id": "observer-attach", - "session_id": session_id, - }) - mobile_socket.send_json( - _command( - "01J00000000000000000000022", - "message.send", - epoch, - session_id=session_id, - payload={ - "client_message_id": "01J00000000000000000000022", - "session_id": session_id, - "text": "Mobile 写回同一个会话", - "media_refs": [], - "client_created_at": datetime.now(timezone.utc).isoformat(), - }, - ) - ) - mobile_reply_frames = [mobile_socket.receive_json() for _ in range(5)] - assert [frame["type"] for frame in mobile_reply_frames] == [ - "turn.started", - "react.thinking.delta", - "answer.delta", - "message.final", - "message.send.ok", - ] - bus.wait_for_count(2) - web_reply_frames = [web_socket.receive_json() for _ in range(5)] - observer_reply_frames = [ - observing_web_socket.receive_json() for _ in range(5) - ] - assert [frame["type"] for frame in web_reply_frames] == [ - "turn.started", - "react.thinking.delta", - "answer.delta", - "turn.output.completed", - "message.final", - ] - assert web_reply_frames[0]["client_message_id"] == ( - "01J00000000000000000000022" - ) - assert web_reply_frames[0]["content"] == "Mobile 写回同一个会话" - assert observer_reply_frames == web_reply_frames - history = client.get(f"/api/chat/sessions/{session_id}/messages").json() - assert [item["content"] for item in history["items"]] == [ - "Web 写入同一个会话", - "Mobile 写回同一个会话", - ] - sessions = client.get("/api/chat/sessions").json() - assert [item["key"] for item in sessions["items"]] == [session_id] - assert not any( - item["key"].startswith(("web:", "mobile:")) - for item in sessions["items"] - ) - - # 4. One logical proactive/schedule result fans out to both UIs. - delivery_id = "schedule-delivery-e2e" - - async def sender(request: DurableDeliveryRequest, started: Any) -> Any: - started( - DurableBindingAttempt( - delivery_id, - "isolated-e2e-snapshot", - "isolated-e2e-generation", - "shared-e2e-binding", - ) - ) - delivery_metadata = cast( - dict[str, JsonValue], dict(request.metadata) - ) - delivery_metadata["delivery_id"] = request.logical_delivery_id - session_message_id = await manager.append_durable_delivery( - session_key=request.projection_session_id, - content=request.body, - delivery_id=request.logical_delivery_id, - control_turn_id=request.accepted_turn.turn_id, - ) - provider = await adapter.deliver( - ProviderDeliveryRequest( - binding_token="shared-e2e-binding", - delivery_id=request.logical_delivery_id, - recipient=request.recipient, - body=request.body, - metadata=delivery_metadata, - commit_role=ChannelCommitRole.DIRECT, - control_turn_id=request.accepted_turn.turn_id, - session_message_id=session_message_id, - ) - ) - return ChannelDeliveryReceipt( - provider.delivery_id, - provider.status, - provider.provider_ids, - provider.error, - ) - - async def project(request: DurableDeliveryRequest) -> str: - return await manager.append_durable_delivery( - session_key=request.projection_session_id, - content=request.body, - delivery_id=request.logical_delivery_id, - control_turn_id=request.accepted_turn.turn_id, - ) - - durable = PluginDurableDeliveries( - DurableDeliveryStore(root / "runtime" / "settlements.sqlite"), - sender, - project, - ) - request = DurableDeliveryRequest( - logical_delivery_id=delivery_id, - accepted_turn=TurnAcceptedReceipt( - "scheduler:morning", - "turn:schedule-e2e", - ), - target_service="scheduler.delivery.v1", - channel="akashic", - recipient=session_id.removeprefix("akashic:"), - projection_session_id=session_id, - body="定时任务完成", - metadata={"source": "schedule"}, - ) - receipt = client.portal.call(durable.submit, request) - assert receipt.state == "projected" - web_delivery = web_socket.receive_json() - mobile_delivery = mobile_socket.receive_json() - assert web_delivery["type"] == "message.final" - assert mobile_delivery["type"] == "session.updated" - assert web_delivery["session_id"] == session_id - assert mobile_delivery["session_id"] == session_id - assert web_delivery["content"] == "定时任务完成" - assert mobile_delivery["payload"]["head_seq"] == 2 - assert web_delivery["metadata"]["delivery_id"] == delivery_id - assert mobile_delivery["payload"]["message_id"] == f"{session_id}:2" - projected = manager.control_store.fetch_session_messages(session_id) - assert [item["content"] for item in projected] == [ - "Web 写入同一个会话", - "Mobile 写回同一个会话", - "定时任务完成", - ] - assert projected[-1]["delivery_id"] == delivery_id - - duplicate = client.portal.call(durable.submit, request) - assert duplicate.projection_message_id == receipt.projection_message_id - assert ( - len(manager.control_store.fetch_session_messages(session_id)) == 3 - ) - finally: - asyncio.run(adapter.stop()) - asyncio.run(channel.stop()) - manager.close() - runtime.close() - - -def test_production_channel_binding_persists_ingress_and_routes_durable_delivery( - tmp_path: Path, -) -> None: - """Run Web ingress and durable output through the committed Core binding.""" - - root = tmp_path / "production-akashic-e2e" - manager = SessionManager(root / "workspace") - - async def build_runtime() -> tuple[MobileGatewayRuntime, object]: - return build_mobile_gateway_runtime( - _config(root), - root, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build_runtime()) - web = WebChatChannel() - channel = AkashicChannel(web, runtime.channel) - bus = MessageBus() - event_bus = EventBus() - plugin_manager = PluginManager( - plugin_dirs=[root / "plugins"], - event_bus=event_bus, - workspace=root / "workspace", - session_manager=manager, - installed_cache_root=root / "cache", - ) - - async def execute(request: Any) -> ControlExecutionResult: - return ControlExecutionResult(response=f"Core 回复:{request.input}") - - conversation = ConversationRuntime(manager.control_store, execute) - worker = PassiveMessageWorker( - bus, - conversation, - cast(Any, SimpleNamespace(session_manager=manager)), - ) - context = cast( - Any, - SimpleNamespace( - bus=bus, - session_manager=manager, - event_bus=event_bus, - push_tool=_PushTool(), - interrupt_controller=None, - attachment_store=AttachmentStore(root / "attachments"), - ), - ) - tasks: tuple[asyncio.Task[None], asyncio.Task[None]] | None = None - - async def start() -> None: - nonlocal tasks - bus.bind_durable_inbound_store(manager.control_store) - plugin_manager.channel_generation_host.bind_inbound_publisher( - bus.publish_channel_inbound - ) - plugin_manager.bind_durable_delivery_sender( - lambda request, started: _dispatch_v3_durable_delivery( - plugin_manager, - bus, - request, - started, - session_manager=manager, - ) - ) - bus.bind_channel_outbound_dispatcher( - plugin_manager.channel_generation_host.dispatch_outbound - ) - await channel.start(context) - await plugin_manager.bind_core_channel_definitions( - (build_core_channel_definition(channel),) - ) - tasks = ( - asyncio.create_task(worker.run()), - asyncio.create_task(bus.dispatch_outbound()), - ) - - async def stop() -> None: - worker.stop() - bus.stop() - if tasks is not None: - await asyncio.gather(*tasks) - await conversation.shutdown() - await plugin_manager.terminate_all() - await channel.stop() - - app = create_chat_app(workspace=root, channel=web) - try: - with TestClient(app) as client: - client.portal.call(start) - with client.websocket_connect("/ws") as socket: - socket.send_json({"type": "session.create", "request_id": "create"}) - session_id = cast(str, socket.receive_json()["session_id"]) - socket.send_json( - { - "type": "message.send", - "request_id": "message", - "session_id": session_id, - "text": "穿过生产 Core", - "media": [], - } - ) - terminal = socket.receive_json() - assert terminal["type"] == "message.final" - assert terminal["content"] == "Core 回复:穿过生产 Core" - turns = manager.control_store.list_turns(session_id) - assert len(turns) == 1 - assert turns[0].input == "穿过生产 Core" - assert turns[0].final_response == "Core 回复:穿过生产 Core" - assert turns[0].metadata["channel"] == "akashic" - - durable = plugin_manager._formal_durable_deliveries() - request = DurableDeliveryRequest( - logical_delivery_id="wake:production-e2e", - accepted_turn=TurnAcceptedReceipt( - "wake:production", - "turn:wake-production", - ), - target_service="wake.delivery.v1", - channel="akashic", - recipient=session_id.removeprefix("akashic:"), - projection_session_id=session_id, - body="主动结果穿过生产 Core", - ) - receipt = client.portal.call(durable.submit, request) - pushed = socket.receive_json() - assert receipt.state == "projected" - assert pushed["type"] == "message.final" - assert pushed["content"] == request.body - assert [ - item["content"] - for item in manager.control_store.fetch_session_messages(session_id) - ] == ["主动结果穿过生产 Core"] - client.portal.call(stop) - finally: - if tasks is not None and any(not task.done() for task in tasks): - for task in tasks: - task.cancel() - with suppress(Exception): - asyncio.run(plugin_manager.terminate_all()) - with suppress(Exception): - asyncio.run(channel.stop()) - manager.close() - runtime.close() - - -def test_isolated_gateway_recovers_lost_frames_and_keeps_history_idempotent( - tmp_path: Path, -) -> None: - """覆盖隔离存储、重复历史同步、断线补发与固定媒体下载。""" - - # 1. 只在 pytest 临时根目录创建 Gateway、会话库和附件目录 - root = tmp_path / "isolated-mobile-e2e" - manager = SessionManager(root / "workspace") - reply_media = root / "fixtures" / "gateway-reply.gif" - reply_media.parent.mkdir(parents=True) - reply_bytes = b"GIF89a" + bytes(range(256)) * 128 - reply_media.write_bytes(reply_bytes) - - async def build_runtime() -> tuple[MobileGatewayRuntime, object]: - return build_mobile_gateway_runtime( - _config(root), - root, - master_keys=_EphemeralMasterKeys(), - ) - - runtime, _ = asyncio.run(build_runtime()) - bus = _DeterministicAgentBus(manager, reply_media) - bus.bind(runtime) - asyncio.run( - runtime.channel.start( - cast( - Any, - SimpleNamespace( - bus=bus, - session_manager=manager, - event_bus=_EventBus(), - push_tool=_PushTool(), - interrupt_controller=None, - attachment_store=AttachmentStore(root / "attachments"), - ), - ) - ) - ) - adapter = asyncio.run( - _attach_open_mobile_v3( - runtime.channel, - bus, - binding_token="isolated-e2e-fixture", - ) - ) - - device_key = ec.generate_private_key(ec.SECP256R1()) - device_id = uuid4().hex - runtime.storage.register_device( - DeviceRecord( - device_id=device_id, - public_key=_public_key(device_key), - display_name="Isolated Android Harness", - created_at=datetime.now(timezone.utc), - revoked_at=None, - capabilities=("stream-v1", "attachments-v1"), - ) - ) - client = TestClient(create_mobile_gateway_app(runtime)) - try: - # 2. 连续拉取同一历史页两次,按 canonical identity 合并后不增长 - with client.websocket_connect("/ws") as websocket: - epoch = _authenticate(websocket, device_id, device_key) - initial = _resume(websocket, epoch, last_ack=0) - assert [frame["type"] for frame in initial] == ["sync.completed"] - websocket.send_json( - { - "v": 1, - "kind": "ack", - "type": "event.ack", - "connection_epoch": epoch, - "payload": {"through_event_seq": initial[-1]["event_seq"]}, - } - ) - last_ack = int(initial[-1]["event_seq"]) - - # 3. Mobile 与 Web 使用同一个 Core 分配规则,不在测试里手写 Session ID。 - websocket.send_json( - _command( - "01J00000000000000000000010", - "session.create", - epoch, - ) - ) - created = websocket.receive_json() - assert created["type"] == "session.created" - session_id = cast(str, created["session_id"]) - assert re.fullmatch(r"akashic:[0-9a-f]{32}", session_id) - assert created["payload"] == {"session_id": session_id} - assert not manager.session_exists(session_id) - historical = manager.get_or_create(session_id) - historical.add_message( - "user", - "隔离历史问题", - client_message_id="01J00000000000000000000000", - ) - historical.add_message("assistant", "隔离历史回答") - manager.save(historical) - - mirror: dict[str, dict[str, Any]] = {} - history_pages: list[list[dict[str, Any]]] = [] - for command_id in ( - "01J00000000000000000000001", - "01J00000000000000000000002", - ): - websocket.send_json( - _command( - command_id, - "history.get", - epoch, - session_id=session_id, - payload={"page": 1, "page_size": 50}, - ) - ) - page = websocket.receive_json() - reply = websocket.receive_json() - assert page["type"] == "history.page" - assert reply["type"] == "history.get.ok" - items = cast(list[dict[str, Any]], page["payload"]["items"]) - history_pages.append(items) - mirror.update({_history_identity(item): item for item in items}) - last_ack = int(page["event_seq"]) - websocket.send_json( - { - "v": 1, - "kind": "ack", - "type": "event.ack", - "connection_epoch": epoch, - "payload": {"through_event_seq": last_ack}, - } - ) - assert len(mirror) == 2 - assert [item["id"] for item in history_pages[0]] == [ - item["id"] for item in history_pages[1] - ] - assert history_pages[0][0]["client_message_id"] == ( - "01J00000000000000000000000" - ) - - # 4. 发送后只读到 turn.started 即断线,模拟移动网络丢帧 - live_command_id = "01J00000000000000000000003" - websocket.send_json( - _command( - live_command_id, - "message.send", - epoch, - session_id=session_id, - payload={ - "client_message_id": live_command_id, - "session_id": session_id, - "text": "请返回固定媒体", - "media_refs": [], - "client_created_at": datetime.now(timezone.utc).isoformat(), - }, - ) - ) - first_live = websocket.receive_json() - assert first_live["type"] == "turn.started" - dropped_final = websocket.receive_json() - dropped_reply = websocket.receive_json() - assert dropped_final["type"] == "message.final" - assert dropped_reply["type"] == "message.send.ok" - - # 4. 新 epoch 从上一个已处理历史页补发,最终回复和附件均不丢失 - with client.websocket_connect("/ws") as websocket: - epoch = _authenticate(websocket, device_id, device_key) - replay = _resume(websocket, epoch, last_ack=last_ack) - assert [frame["type"] for frame in replay] == [ - "turn.started", - "message.final", - "sync.completed", - ] - final = replay[1] - assert final["payload"]["content"] == "隔离网关固定回复" - descriptor = final["payload"]["attachments"][0] - terminal_seq = int(replay[-1]["event_seq"]) - websocket.send_json( - { - "v": 1, - "kind": "ack", - "type": "event.ack", - "connection_epoch": epoch, - "payload": {"through_event_seq": terminal_seq}, - } - ) - - download_id = "01J00000000000000000000004" - websocket.send_json( - _command( - download_id, - "attachment.download", - epoch, - session_id=session_id, - payload={ - "attachment_id": descriptor["attachment_id"], - "offset": 0, - }, - ) - ) - chunk = decode_attachment_chunk(websocket.receive_bytes()) - download_reply = websocket.receive_json() - assert download_reply["type"] == "attachment.download.ok" - assert chunk.data == reply_bytes - assert hashlib.sha256(chunk.data).hexdigest() == descriptor["sha256"] - - # 5. 重连后的全量历史仍只对应四条 canonical message - websocket.send_json( - _command( - "01J00000000000000000000005", - "history.get", - epoch, - session_id=session_id, - payload={"page": 1, "page_size": 50}, - ) - ) - refreshed = websocket.receive_json() - assert websocket.receive_json()["type"] == "history.get.ok" - refreshed_items = cast(list[dict[str, Any]], refreshed["payload"]["items"]) - mirror.update({_history_identity(item): item for item in refreshed_items}) - assert len(refreshed_items) == 4 - assert len(mirror) == 4 - assert bus.inbound_count == 1 - assert bus.legacy_publish_calls == 0 - - # 6. 所有持久化路径必须位于 pytest 隔离根目录 - assert (root / "gateway" / "mobile.db").is_file() - assert (root / "workspace" / "sessions.db").is_file() - assert (root / "attachments").is_dir() - assert all(root in path.parents for path in root.rglob("*")) - finally: - asyncio.run(adapter.stop()) - asyncio.run(runtime.channel.stop()) - manager.close() - runtime.close() diff --git a/tests/mobile_realtime/test_isolated_gateway_faults.py b/tests/mobile_realtime/test_isolated_gateway_faults.py deleted file mode 100644 index 4ae172e18..000000000 --- a/tests/mobile_realtime/test_isolated_gateway_faults.py +++ /dev/null @@ -1,148 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -from datetime import datetime, timezone -from types import SimpleNamespace -from typing import Any, cast - -import pytest - -from agent.plugin_composition.channels import ChannelInboundMessage, RawInbound -from session.manager import SessionManager -from tests_scenarios.mobile_isolated_gateway import ( - FixedReplyBus, - GatewayFaultController, - load_replay_turn, -) - - -def test_before_challenge_fault_waits_for_pairing_and_triggers_once() -> None: - controller = GatewayFaultController("stall_before_challenge") - - assert not controller.claim_before_challenge(has_paired_device=False) - assert controller.claim_before_challenge(has_paired_device=True) - assert not controller.claim_before_challenge(has_paired_device=True) - assert not controller.claim_after_auth() - - -def test_after_auth_fault_triggers_once() -> None: - controller = GatewayFaultController("stall_after_auth") - - assert not controller.claim_before_challenge(has_paired_device=True) - assert controller.claim_after_auth() - assert not controller.claim_after_auth() - - -def test_fault_controller_rejects_unknown_mode() -> None: - with pytest.raises(ValueError, match="未知隔离 Gateway 故障模式"): - GatewayFaultController("drop_everything") - - -def test_load_replay_turn_validates_and_preserves_real_stage_order(tmp_path: Any) -> None: - replay_path = tmp_path / "turn.json" - _ = replay_path.write_text( - json.dumps( - [ - {"role": "user", "content": "问题"}, - { - "role": "assistant", - "content": "最终回答", - "tool_chain": json.dumps( - [ - { - "reasoning_content": "先思考", - "text": "中间说明", - "calls": [ - { - "call_id": "call-1", - "name": "inspect", - "status": "success", - "arguments": {"step": 1}, - "final_arguments": {"step": 1}, - "result": "完成", - } - ], - } - ] - ), - }, - ], - ensure_ascii=False, - ), - encoding="utf-8", - ) - - replay = load_replay_turn(replay_path) - - assert replay.content == "最终回答" - assert replay.reasoning == "先思考" - assert replay.call_count == 1 - assert replay.stages[0].text == "中间说明" - assert replay.stages[0].calls[0].name == "inspect" - - -def test_performance_fixture_can_emit_one_character_provider_deltas(tmp_path: Any) -> None: - manager = SessionManager(tmp_path / "workspace") - media = tmp_path / "fixed.gif" - _ = media.write_bytes(b"fixture") - bus = FixedReplyBus( - manager, - media, - tokens_per_second=100, - stream_tokens=12, - stream_chunk_chars=1, - ) - - try: - thinking, _, answer, thinking_delay, answer_delay = bus._stream_payloads() # pyright: ignore[reportPrivateUsage] - assert all(len(delta) == 1 for delta in (*thinking, *answer)) - assert len(thinking) + len(answer) == 12 - assert thinking_delay == pytest.approx(0.01) - assert answer_delay == pytest.approx(0.01) - finally: - manager.close() - - -@pytest.mark.asyncio -async def test_fixed_reply_is_persisted_before_v3_admit_returns(tmp_path: Any) -> None: - manager = SessionManager(tmp_path / "workspace") - media = tmp_path / "fixed.gif" - _ = media.write_bytes(b"fixture") - started = asyncio.Event() - - class BlockingChannel: - name = "mobile" - - async def _on_turn_started(self, event: object) -> None: - started.set() - await asyncio.Event().wait() - - bus = FixedReplyBus(manager, media) - bus.bind(cast(Any, SimpleNamespace(channel=BlockingChannel()))) - raw = RawInbound( - message_id="client-message", - provider_identity="mobile:akashic:test", - recipient="mobile:akashic:test", - message=ChannelInboundMessage( - channel="mobile", - sender="device:test", - chat_id="mobile:akashic:test", - content="问题", - timestamp=datetime.now(timezone.utc), - metadata={ - "client_message_id": "client-message", - "session_key_override": "akashic:test", - }, - ), - ) - - try: - assert await bus.admit(raw) - session = manager.get_existing("akashic:test") - assert [message["role"] for message in session.messages] == ["user", "assistant"] - assert session.messages[0]["client_message_id"] == "client-message" - await asyncio.wait_for(started.wait(), timeout=1) - finally: - await bus.aclose() - manager.close() diff --git a/tests/mobile_realtime/test_message_content_http.py b/tests/mobile_realtime/test_message_content_http.py deleted file mode 100644 index b8e99f2ec..000000000 --- a/tests/mobile_realtime/test_message_content_http.py +++ /dev/null @@ -1,245 +0,0 @@ -from __future__ import annotations - -import hashlib -from datetime import datetime, timedelta, timezone -from pathlib import Path -from types import SimpleNamespace -from typing import cast - -import pytest -from cryptography.hazmat.primitives.asymmetric import ec -from fastapi.testclient import TestClient - -from infra.mobile_realtime.channel import _fit_mobile_history_payload -from infra.mobile_realtime.gateway import ( - MobileMessageContentHttpError, - _parse_message_content_range, - create_mobile_gateway_app, -) -from infra.mobile_realtime.key_protection import LoadedKeyset -from infra.mobile_realtime.message_content_http import ( - MessageContentTicketError, - MessageContentTicketIssuer, -) -from infra.mobile_realtime.storage import MobileRealtimeStorage -from session.store import SessionStore - -_BASE64URL_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" - - -class _Storage: - def read_device(self, device_id: str) -> object | None: - if device_id != "device-1": - return None - return SimpleNamespace(revoked_at=None) - - -def _issuer(now: datetime) -> MessageContentTicketIssuer: - keyset = SimpleNamespace( - manifest=SimpleNamespace(server_id="server-1"), - identity_private_key=ec.generate_private_key(ec.SECP256R1()), - ) - return MessageContentTicketIssuer( - cast(LoadedKeyset, keyset), - cast(MobileRealtimeStorage, _Storage()), - clock=lambda: now, - ) - - -def test_ticket_binds_device_message_digest_and_length() -> None: - now = datetime(2026, 8, 2, tzinfo=timezone.utc) - issuer = _issuer(now) - grant = issuer.issue( - device_id="device-1", - connection_epoch=7, - session_id="akashic:test", - message_id="akashic:test:3", - byte_length=300_000, - sha256="a" * 64, - ) - - verified = issuer.verify(grant.ticket) - - assert verified.device_id == "device-1" - assert verified.connection_epoch == 7 - assert verified.session_id == "akashic:test" - assert verified.message_id == "akashic:test:3" - assert verified.byte_length == 300_000 - assert verified.sha256 == "a" * 64 - - -def test_ticket_rejects_tampering() -> None: - now = datetime(2026, 8, 2, tzinfo=timezone.utc) - issuer = _issuer(now) - grant = issuer.issue( - device_id="device-1", - connection_epoch=7, - session_id="akashic:test", - message_id="akashic:test:3", - byte_length=1, - sha256="b" * 64, - ) - - payload, signature = grant.ticket.split(".") - tampered_first = "B" if signature[0] == "A" else "A" - with pytest.raises(MessageContentTicketError, match="签名无效"): - issuer.verify(f"{payload}.{tampered_first}{signature[1:]}") - - -def _padding_bit_alias(value: str) -> str: - remainder = len(value) % 4 - assert remainder in (2, 3) - padding_mask = 0b1111 if remainder == 2 else 0b11 - tail_index = _BASE64URL_ALPHABET.index(value[-1]) - assert tail_index & padding_mask == 0 - alias_index = tail_index | 1 - assert alias_index != tail_index - return value[:-1] + _BASE64URL_ALPHABET[alias_index] - - -def test_ticket_rejects_noncanonical_payload_padding_bits() -> None: - now = datetime(2026, 8, 2, tzinfo=timezone.utc) - issuer = _issuer(now) - grant = issuer.issue( - device_id="device-1", - connection_epoch=7, - session_id="akashic:test", - message_id="akashic:test:3", - byte_length=1, - sha256="b" * 64, - ) - payload, signature = grant.ticket.split(".") - - with pytest.raises(MessageContentTicketError, match="Base64URL 无效"): - issuer.verify(f"{_padding_bit_alias(payload)}.{signature}") - - -def test_range_is_single_bounded_and_clamped() -> None: - assert _parse_message_content_range("bytes=10-19", 100) == (10, 19) - assert _parse_message_content_range("bytes=90-200", 100) == (90, 99) - with pytest.raises(MobileMessageContentHttpError, match="单个 bytes Range"): - _parse_message_content_range(None, 100) - with pytest.raises(MobileMessageContentHttpError, match="单次下载预算"): - _parse_message_content_range("bytes=0-262144", 300_000) - - -def test_http_route_returns_identity_encoded_verified_range_headers() -> None: - content = "界🌙".encode() - sha256 = hashlib.sha256(content).hexdigest() - - class Runtime: - def read_message_content_http( - self, - *, - ticket: str, - range_header: str | None, - if_range: str | None, - ) -> tuple[bytes, int, int, int, str]: - assert ticket == "ticket" - assert range_header == f"bytes=0-{len(content) - 1}" - assert if_range == f'"{sha256}"' - return content, 0, len(content) - 1, len(content), sha256 - - client = TestClient(create_mobile_gateway_app(Runtime())) # type: ignore[arg-type] - response = client.get( - "/mobile/message-content/v1", - headers={ - "Authorization": "Bearer ticket", - "Range": f"bytes=0-{len(content) - 1}", - "If-Range": f'"{sha256}"', - }, - ) - - assert response.status_code == 206 - assert response.content == content - assert response.headers["content-encoding"] == "identity" - assert response.headers["content-range"] == f"bytes 0-{len(content) - 1}/{len(content)}" - assert response.headers["etag"] == f'"{sha256}"' - assert response.headers["content-digest"].startswith("sha-256=:") - assert response.headers["repr-digest"].startswith("sha-256=:") - - -def test_history_externalizes_unicode_content_without_losing_tool_projection() -> None: - content = "花月🌙" * 80_000 - tool_chain = [ - { - "reasoning_content": "先检查真实状态", - "calls": [{"name": "shell", "status": "success", "description": "核对日志"}], - } - ] - payload: dict[str, object] = { - "items": [{"id": "m", "content": content, "tool_chain": tool_chain}], - "total": 1, - "page_size": 10, - "content_ref_version": 1, - "after_seq": -1, - "next_after_seq": 0, - "snapshot_max_seq": 0, - "has_more": False, - } - - _fit_mobile_history_payload(payload, allow_content_refs=True) - - item = payload["items"][0] # type: ignore[index] - assert item["content"] is None - assert item["tool_chain"] == tool_chain - assert item["content_ref"] == { - "version": 1, - "encoding": "utf-8", - "byte_length": len(content.encode("utf-8")), - "sha256": hashlib.sha256(content.encode("utf-8")).hexdigest(), - "preview": content[:512], - } - - -def test_mobile_history_cursor_freezes_append_high_water(tmp_path: Path) -> None: - store = SessionStore(tmp_path / "sessions.db") - try: - store.persist_session( - "akashic:test", - created_at="2026-08-02T00:00:00+00:00", - updated_at="2026-08-02T00:00:00+00:00", - metadata={}, - messages=[ - { - "role": "user", - "content": "one", - "timestamp": "2026-08-02T00:00:00+00:00", - "extra": {}, - }, - { - "role": "assistant", - "content": "two", - "timestamp": "2026-08-02T00:00:01+00:00", - "extra": {}, - }, - ], - ) - total, snapshot = store.mobile_history_snapshot("akashic:test") - store.persist_session( - "akashic:test", - created_at="2026-08-02T00:00:00+00:00", - updated_at="2026-08-02T00:00:02+00:00", - metadata={}, - messages=[ - { - "role": "assistant", - "content": "later", - "timestamp": "2026-08-02T00:00:02+00:00", - "extra": {}, - }, - ], - ) - - page = store.list_mobile_history_page( - session_key="akashic:test", - after_seq=-1, - through_seq=snapshot, - page_size=10, - ) - - assert total == 2 - assert snapshot == 1 - assert [item["content"] for item in page] == ["one", "two"] - finally: - store.close() diff --git a/tests/mobile_realtime/test_mobile_realtime_protocol.py b/tests/mobile_realtime/test_mobile_realtime_protocol.py deleted file mode 100644 index 7e95dd611..000000000 --- a/tests/mobile_realtime/test_mobile_realtime_protocol.py +++ /dev/null @@ -1,463 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import pytest -from pydantic import ValidationError - -from infra.mobile_realtime.protocol import ( - AttachmentDownloadCommand, - AuthAcceptedControl, - CONTROL_TYPES, - EVENT_TYPES, - GenericControl, - MessageSendCommand, - PRE_AUTH_CONTROL_TYPES, - ProtocolDecodeError, - ReplyFrame, - ThinkingDeltaEvent, - frame_to_json, - parse_frame, -) -from scripts.generate_mobile_realtime_schema import OUTPUT, build_schema - -FIXTURES = Path(__file__).parent / "fixtures" / "frames-v1.json" - - -def test_golden_frames_round_trip() -> None: - frames = json.loads(FIXTURES.read_text(encoding="utf-8")) - parsed = [parse_frame(json.dumps(frame, ensure_ascii=False)) for frame in frames] - - assert isinstance(parsed[0], MessageSendCommand) - assert isinstance(parsed[1], ReplyFrame) - assert isinstance(parsed[4], AuthAcceptedControl) - assert [json.loads(frame_to_json(frame)) for frame in parsed] == frames - - -def test_resume_rejects_ack_that_cannot_fit_sqlite_sequence() -> None: - frame = _golden_frame(5) - frame["payload"]["last_ack"] = (1 << 63) - 2 - - with pytest.raises(ValidationError, match="less than or equal"): - parse_frame(json.dumps(frame)) - - -def test_message_send_rejects_mismatched_session() -> None: - frame = _golden_frame(0) - frame["session_id"] = "akashic:other" - - with pytest.raises(ValidationError, match="session_id 必须一致"): - parse_frame(json.dumps(frame)) - - -def test_attachment_download_validates_offset() -> None: - frame = { - "v": 1, - "kind": "command", - "type": "attachment.download", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "session_id": "akashic:test", - "payload": { - "attachment_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", - "offset": 131072, - }, - } - - parsed = parse_frame(json.dumps(frame)) - assert isinstance(parsed, AttachmentDownloadCommand) - assert parsed.payload.offset == 131072 - - frame["payload"]["offset"] = -1 - with pytest.raises(ValidationError): - parse_frame(json.dumps(frame)) - - -def test_message_send_rejects_duplicate_or_too_many_media_refs() -> None: - duplicate = _golden_frame(0) - duplicate["payload"]["media_refs"] = [ - "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "01ARZ3NDEKTSV4RRFFQ69G5FAV", - ] - with pytest.raises(ValidationError, match="不能重复"): - parse_frame(json.dumps(duplicate)) - - oversized = _golden_frame(0) - oversized["payload"]["media_refs"] = [ - f"01ARZ3NDEKTSV4RRFFQ69G5FA{suffix}" for suffix in "0123456789A" - ] - with pytest.raises(ValidationError): - parse_frame(json.dumps(oversized)) - - -def test_message_send_validates_reply_identity() -> None: - valid = _golden_frame(0) - valid["payload"]["reply_to"] = { - "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAW", - } - parsed = parse_frame(json.dumps(valid)) - assert isinstance(parsed, MessageSendCommand) - assert parsed.payload.reply_to is not None - assert parsed.payload.reply_to.client_message_id == "01ARZ3NDEKTSV4RRFFQ69G5FAW" - - delivery = _golden_frame(0) - delivery["payload"]["reply_to"] = {"delivery_id": "delivery-1"} - parsed_delivery = parse_frame(json.dumps(delivery)) - assert isinstance(parsed_delivery, MessageSendCommand) - assert parsed_delivery.payload.reply_to is not None - assert parsed_delivery.payload.reply_to.delivery_id == "delivery-1" - - invalid = _golden_frame(0) - invalid["payload"]["reply_to"] = { - "message_id": "akashic:test:1", - "delivery_id": "delivery-1", - } - with pytest.raises(ValidationError, match="只能提供一种"): - parse_frame(json.dumps(invalid)) - - -def test_message_send_validates_explicit_retry_identity() -> None: - frame = _golden_frame(0) - frame["payload"]["retry_of_client_message_id"] = "01ARZ3NDEKTSV4RRFFQ69G5FAW" - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, MessageSendCommand) - assert parsed.payload.retry_of_client_message_id == "01ARZ3NDEKTSV4RRFFQ69G5FAW" - - frame["payload"]["retry_of_client_message_id"] = frame["payload"][ - "client_message_id" - ] - with pytest.raises(ValidationError, match="必须指向既有消息"): - parse_frame(json.dumps(frame)) - - -def test_message_send_accepts_real_client_creation_time() -> None: - frame = _golden_frame(0) - frame["payload"]["client_created_at"] = "2026-07-16T12:34:56+08:00" - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, MessageSendCommand) - assert parsed.payload.client_created_at == "2026-07-16T12:34:56+08:00" - - -def test_message_send_validates_model_selection() -> None: - frame = _golden_frame(0) - frame["payload"]["model_runtime_id"] = " model-a " - frame["payload"]["model_reasoning_effort"] = " high " - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, MessageSendCommand) - assert parsed.payload.model_runtime_id == "model-a" - assert parsed.payload.model_reasoning_effort == "high" - - frame["payload"]["model_runtime_id"] = "" - with pytest.raises(ValidationError, match="model_reasoning_effort"): - parse_frame(json.dumps(frame)) - - -@pytest.mark.parametrize( - "value", - ( - "not-a-time", - "2026-07-16T12:34:56", - "2026-07-16 12:34:56+08:00", - "20260716T123456+0800", - "2026-07-16T12:34:56,5+08:00", - ), -) -def test_message_send_rejects_invalid_client_creation_time(value: str) -> None: - frame = _golden_frame(0) - frame["payload"]["client_created_at"] = value - - with pytest.raises(ValidationError, match="client_created_at"): - parse_frame(json.dumps(frame)) - - -def test_message_send_accepts_but_drops_legacy_reply_projection() -> None: - frame = _golden_frame(0) - frame["payload"]["reply_to"] = { - "message_id": "akashic:test:1", - "role": "assistant", - "preview": "旧版客户端缓存的摘要", - } - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, MessageSendCommand) - assert parsed.payload.reply_to is not None - assert parsed.payload.reply_to.legacy_role == "assistant" - assert parsed.payload.reply_to.legacy_preview == "旧版客户端缓存的摘要" - dumped = parsed.payload.reply_to.model_dump(by_alias=True) - assert "role" not in dumped - assert "preview" not in dumped - - -@pytest.mark.parametrize( - ("field", "value"), - ( - ("v", 2), - ("kind", "notification"), - ("type", "unknown.command"), - ("id", "not-a-valid-id"), - ("connection_epoch", 0), - ), -) -def test_command_rejects_invalid_envelope(field: str, value: object) -> None: - frame = _golden_frame(0) - frame[field] = value - - with pytest.raises(ValidationError): - parse_frame(json.dumps(frame)) - - -def test_event_rejects_unknown_type_and_missing_sequence() -> None: - frame = _golden_frame(2) - frame["type"] = "answer.replaced" - frame.pop("event_seq") - - with pytest.raises(ValidationError): - parse_frame(json.dumps(frame)) - - -def test_session_list_is_a_valid_server_event() -> None: - frame = { - "v": 1, - "kind": "event", - "type": "session.list", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "event_seq": 1, - "payload": {"items": []}, - } - - assert parse_frame(json.dumps(frame)).type == "session.list" - - -def test_command_list_command_and_reply_are_valid() -> None: - command = { - "v": 1, - "kind": "command", - "type": "command.list", - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "payload": {}, - } - reply = { - **command, - "kind": "reply", - "type": "command.list.ok", - "payload": { - "items": [ - {"command": "memorystatus", "description": "查看记忆整理状态"}, - ], - }, - } - - assert parse_frame(json.dumps(command)).type == "command.list" - assert parse_frame(json.dumps(reply)).type == "command.list.ok" - - -@pytest.mark.parametrize( - "command_type", - ( - "runtime.document.list", - "runtime.document.get", - "runtime.capability.list", - "runtime.mcp.get", - "scheduler.job.list", - "scheduler.job.get", - ), -) -def test_runtime_inspection_command_and_reply_are_valid(command_type: str) -> None: - command = { - "v": 1, - "kind": "command", - "type": command_type, - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "payload": {}, - } - reply = { - **command, - "kind": "reply", - "type": f"{command_type}.ok", - } - - assert parse_frame(json.dumps(command)).type == command_type - assert parse_frame(json.dumps(reply)).type == f"{command_type}.ok" - - -@pytest.mark.parametrize( - "command_type", - ( - "plugin.ui.catalog", - "plugin.ui.asset.get", - "plugin.ui.query", - "plugin.ui.cancel", - ), -) -def test_plugin_ui_commands_are_valid(command_type: str) -> None: - frame = { - "v": 1, - "kind": "command", - "type": command_type, - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "payload": {}, - } - - assert parse_frame(json.dumps(frame)).type == command_type - - -@pytest.mark.parametrize( - "command_type", ("plugin.ui.list", "plugin.ui.asset", "plugin.ui.call") -) -def test_plugin_ui_v1_commands_are_rejected(command_type: str) -> None: - frame = { - "v": 1, - "kind": "command", - "type": command_type, - "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "connection_epoch": 1, - "payload": {}, - } - - with pytest.raises(ValueError): - parse_frame(json.dumps(frame)) - - -def test_delta_process_block_fields_must_appear_together() -> None: - frame = _golden_frame(2) - frame["type"] = "react.thinking.delta" - frame["payload"] = { - "delta": "思考中", - "block_id": "thinking:turn-1:0", - "ordinal": 0, - "control_turn_id": "turn:logical-1", - } - parsed = parse_frame(json.dumps(frame)) - assert isinstance(parsed, ThinkingDeltaEvent) - assert parsed.payload.block_id == "thinking:turn-1:0" - assert parsed.payload.control_turn_id == "turn:logical-1" - - frame["payload"].pop("ordinal") - with pytest.raises(ValidationError, match="必须同时出现"): - parse_frame(json.dumps(frame)) - - -@pytest.mark.parametrize( - "reply_type", - ("message.send", "unknown.ok", "message.send.done"), -) -def test_reply_requires_known_command_suffix(reply_type: str) -> None: - frame = _golden_frame(1) - frame["type"] = reply_type - - with pytest.raises(ValidationError, match="reply type"): - parse_frame(json.dumps(frame)) - - -def test_plugin_ui_catalog_accepts_not_modified_reply() -> None: - frame = _golden_frame(1) - frame["type"] = "plugin.ui.catalog.not_modified" - frame["payload"] = {"catalog_revision": "a" * 64} - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, ReplyFrame) - assert parsed.type == "plugin.ui.catalog.not_modified" - - -def test_session_create_accepts_created_reply() -> None: - frame = _golden_frame(1) - frame["type"] = "session.created" - frame["session_id"] = "akashic:" + "a" * 32 - frame["payload"] = {"session_id": frame["session_id"]} - - parsed = parse_frame(json.dumps(frame)) - - assert isinstance(parsed, ReplyFrame) - assert parsed.type == "session.created" - - -def test_auth_accepted_rejects_epoch_mismatch() -> None: - frame = _golden_frame(4) - frame["payload"]["connection_epoch"] = 8 - - with pytest.raises(ValidationError, match="connection_epoch 必须一致"): - parse_frame(json.dumps(frame)) - - -def test_pair_claim_is_valid_before_authentication() -> None: - frame = parse_frame( - '{"v":1,"kind":"control","type":"pair.claim","payload":{"pairing_id":"p1"}}' - ) - - assert isinstance(frame, GenericControl) - assert frame.connection_epoch is None - assert frame.type in PRE_AUTH_CONTROL_TYPES - - -def test_control_rejects_unknown_type() -> None: - with pytest.raises(ValidationError): - parse_frame('{"v":1,"kind":"control","type":"auth.skipped","payload":{}}') - - -def test_device_revoked_is_control_only() -> None: - assert "device.revoked" in CONTROL_TYPES - assert "device.revoked" not in EVENT_TYPES - control = parse_frame( - '{"v":1,"kind":"control","type":"device.revoked",' - '"connection_epoch":7,"payload":{"device_id":"device-1"}}' - ) - assert isinstance(control, GenericControl) - with pytest.raises(ValidationError): - parse_frame( - '{"v":1,"kind":"event","type":"device.revoked",' - '"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","connection_epoch":7,' - '"event_seq":1,"payload":{"device_id":"device-1"}}' - ) - - -def test_plugin_ui_changed_is_authenticated_connection_control() -> None: - frame = parse_frame( - '{"v":1,"kind":"control","type":"plugin.ui.changed",' - '"connection_epoch":7,"payload":{}}' - ) - - assert frame.type == "plugin.ui.changed" - assert frame.connection_epoch == 7 - - -def test_decoder_rejects_ambiguous_or_non_object_json() -> None: - with pytest.raises(ProtocolDecodeError, match="重复字段"): - parse_frame('{"v":1,"v":1}') - with pytest.raises(ProtocolDecodeError, match="顶层必须是 object"): - parse_frame("[]") - with pytest.raises(ProtocolDecodeError, match="非标准常量"): - parse_frame('{"value":NaN}') - - -def test_models_reject_unknown_fields() -> None: - frame = _golden_frame(3) - frame["unexpected"] = True - - with pytest.raises(ValidationError): - parse_frame(json.dumps(frame)) - - -def test_generated_schema_matches_checked_in_file() -> None: - encoded = ( - json.dumps(build_schema(), ensure_ascii=False, indent=2, sort_keys=True) + "\n" - ) - assert OUTPUT.read_text(encoding="utf-8") == encoded - - -def _golden_frame(index: int) -> dict[str, Any]: - frames = json.loads(FIXTURES.read_text(encoding="utf-8")) - return frames[index] diff --git a/tests/mobile_realtime/test_pairing_auth.py b/tests/mobile_realtime/test_pairing_auth.py deleted file mode 100644 index 2c59d8cd9..000000000 --- a/tests/mobile_realtime/test_pairing_auth.py +++ /dev/null @@ -1,277 +0,0 @@ -from __future__ import annotations - -import base64 -import secrets -from datetime import datetime, timedelta, timezone -from pathlib import Path -from uuid import uuid4 - -import pytest -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import ec - -from infra.mobile_realtime.auth import ( - DeviceAuthenticator, - DeviceProofPayload, - DeviceRevokedError, - UnknownAuthenticationChallenge, - device_proof_signing_bytes, - server_challenge_signing_bytes, -) -from infra.mobile_realtime.key_protection import ( - KeyProtectionError, - KeysetManager, - LoadedKeyset, -) -from infra.mobile_realtime.pairing import ( - PairClaimPayload, - PairingConfirmationError, - PairingSecretError, - PairingService, - PairingSignatureError, - pair_claim_signing_bytes, - parse_device_public_key, -) -from infra.mobile_realtime.storage import MobileRealtimeStorage, PairingStateError - - -class _EphemeralMasterKeys: - def __init__(self) -> None: - self.keys: dict[str, bytes] = {} - - def create(self) -> tuple[str, bytes]: - key_id = uuid4().hex - key = secrets.token_bytes(32) - self.keys[key_id] = key - return key_id, key - - def load(self, master_key_id: str) -> bytes: - try: - return self.keys[master_key_id] - except KeyError as error: - raise KeyProtectionError("测试 master key 不存在") from error - - -def _device_public_key(private_key: ec.EllipticCurvePrivateKey) -> str: - encoded = private_key.public_key().public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - return base64.b64encode(encoded).decode("ascii") - - -def _signed_claim( - service: PairingService, - private_key: ec.EllipticCurvePrivateKey, -) -> tuple[PairClaimPayload, str]: - offer = service.create_offer() - public_key = _device_public_key(private_key) - client_nonce = base64.urlsafe_b64encode(secrets.token_bytes(18)).decode("ascii") - transcript = pair_claim_signing_bytes( - server_id=offer.server_id, - pairing_id=offer.pairing_id, - one_time_secret=offer.one_time_secret, - device_public_key=public_key, - device_name="Pixel Emulator", - capabilities=["stream-v1", "attachments-v1"], - client_nonce=client_nonce, - ) - signature = private_key.sign(transcript, ec.ECDSA(hashes.SHA256())) - return ( - PairClaimPayload( - pairing_id=offer.pairing_id, - one_time_secret=offer.one_time_secret, - device_public_key=public_key, - device_name="Pixel Emulator", - capabilities=["stream-v1", "attachments-v1"], - client_nonce=client_nonce, - signature=base64.b64encode(signature).decode("ascii"), - ), - offer.server_application_key_fingerprint, - ) - - -def _services( - tmp_path: Path, -) -> tuple[MobileRealtimeStorage, PairingService, LoadedKeyset]: - keyset = KeysetManager( - tmp_path / "keys", - _EphemeralMasterKeys(), - ).initialize(lan_hostname="akashic.local") - storage = MobileRealtimeStorage(tmp_path / "mobile.db") - service = PairingService( - storage, - keyset, - lan_endpoints=("wss://akashic.local:6323/ws",), - tunnel_endpoints=("wss://agent.example.com/ws",), - ) - return storage, service, keyset - - -def test_default_pairing_offer_lasts_eight_minutes(tmp_path: Path) -> None: - storage, _service, keyset = _services(tmp_path) - now = datetime(2026, 8, 11, 2, 30, tzinfo=timezone.utc) - service = PairingService( - storage, - keyset, - lan_endpoints=("wss://akashic.local:6323/ws",), - tunnel_endpoints=("wss://mobile.huashen258.cc/ws",), - clock=lambda: now, - ) - - offer = service.create_offer() - - assert offer.expires_at == now + timedelta(minutes=8) - storage.close() - - -def test_pairing_requires_signed_claim_and_desktop_confirmation(tmp_path: Path) -> None: - storage, service, _ = _services(tmp_path) - device_key = ec.generate_private_key(ec.SECP256R1()) - payload, _ = _signed_claim(service, device_key) - - claim = service.claim(payload) - with pytest.raises(PairingConfirmationError, match="确认码不一致"): - service.approve(payload.pairing_id, "000000") - - device = service.approve(payload.pairing_id, claim.confirmation_code) - session = storage.read_pairing_session(payload.pairing_id) - - assert storage.read_device(device.device_id) == device - assert session is not None - assert session.status == "consumed" - assert session.secret_hash is None - assert service.pending_claim(payload.pairing_id) is None - with pytest.raises(PairingStateError, match="不能 claim"): - service.claim(payload) - storage.close() - - -def test_pairing_rejects_wrong_secret_and_signature(tmp_path: Path) -> None: - storage, service, _ = _services(tmp_path) - device_key = ec.generate_private_key(ec.SECP256R1()) - payload, _ = _signed_claim(service, device_key) - - wrong_secret = payload.model_copy( - update={"one_time_secret": "A" * len(payload.one_time_secret)} - ) - with pytest.raises(PairingSecretError, match="secret 无效"): - service.claim(wrong_secret) - - wrong_signature = payload.model_copy( - update={"signature": base64.b64encode(b"invalid-signature" * 5).decode()} - ) - with pytest.raises(PairingSignatureError, match="签名无效"): - service.claim(wrong_signature) - storage.close() - - -def test_device_challenge_authentication_is_signed_one_shot_and_revocable( - tmp_path: Path, -) -> None: - storage, pairing, keyset = _services(tmp_path) - device_key = ec.generate_private_key(ec.SECP256R1()) - claim_payload, _ = _signed_claim(pairing, device_key) - claim = pairing.claim(claim_payload) - device = pairing.approve(claim.pairing_id, claim.confirmation_code) - authenticator = DeviceAuthenticator(storage, keyset) - - challenge = authenticator.create_challenge("connection-1") - server_key = parse_device_public_key(challenge.server_public_key) - server_key.verify( - base64.b64decode(challenge.signature, validate=True), - server_challenge_signing_bytes(challenge), - ec.ECDSA(hashes.SHA256()), - ) - client_nonce = base64.urlsafe_b64encode(secrets.token_bytes(18)).decode("ascii") - proof_bytes = device_proof_signing_bytes( - server_id=challenge.server_id, - challenge_id=challenge.challenge_id, - challenge_nonce=challenge.nonce, - device_id=device.device_id, - client_nonce=client_nonce, - ) - signature = device_key.sign(proof_bytes, ec.ECDSA(hashes.SHA256())) - proof = DeviceProofPayload( - challenge_id=challenge.challenge_id, - device_id=device.device_id, - client_nonce=client_nonce, - signature=base64.b64encode(signature).decode("ascii"), - ) - - authenticated = authenticator.authenticate("connection-1", proof) - assert authenticated.device_id == device.device_id - assert authenticated.connection_epoch == 1 - with pytest.raises(UnknownAuthenticationChallenge): - authenticator.authenticate("connection-1", proof) - - restarted = DeviceAuthenticator(storage, keyset) - restart_challenge = restarted.create_challenge("connection-after-restart") - restart_bytes = device_proof_signing_bytes( - server_id=restart_challenge.server_id, - challenge_id=restart_challenge.challenge_id, - challenge_nonce=restart_challenge.nonce, - device_id=device.device_id, - client_nonce=client_nonce, - ) - restart_signature = device_key.sign( - restart_bytes, - ec.ECDSA(hashes.SHA256()), - ) - after_restart = restarted.authenticate( - "connection-after-restart", - DeviceProofPayload( - challenge_id=restart_challenge.challenge_id, - device_id=device.device_id, - client_nonce=client_nonce, - signature=base64.b64encode(restart_signature).decode("ascii"), - ), - ) - assert after_restart.connection_epoch == 2 - - _ = storage.revoke_device(device.device_id, revoked_at=datetime.now(timezone.utc)) - revoked_challenge = authenticator.create_challenge("connection-2") - revoked_bytes = device_proof_signing_bytes( - server_id=revoked_challenge.server_id, - challenge_id=revoked_challenge.challenge_id, - challenge_nonce=revoked_challenge.nonce, - device_id=device.device_id, - client_nonce=client_nonce, - ) - revoked_signature = device_key.sign(revoked_bytes, ec.ECDSA(hashes.SHA256())) - with pytest.raises(DeviceRevokedError): - authenticator.authenticate( - "connection-2", - DeviceProofPayload( - challenge_id=revoked_challenge.challenge_id, - device_id=device.device_id, - client_nonce=client_nonce, - signature=base64.b64encode(revoked_signature).decode("ascii"), - ), - ) - storage.close() - - -def test_expired_pairing_secret_is_rejected(tmp_path: Path) -> None: - now = datetime(2026, 7, 14, tzinfo=timezone.utc) - current = [now] - keyset = KeysetManager( - tmp_path / "keys", - _EphemeralMasterKeys(), - ).initialize(lan_hostname="akashic.local") - storage = MobileRealtimeStorage(tmp_path / "mobile.db") - service = PairingService( - storage, - keyset, - lan_endpoints=("wss://akashic.local:6323/ws",), - tunnel_endpoints=(), - ttl=timedelta(seconds=1), - clock=lambda: current[0], - ) - device_key = ec.generate_private_key(ec.SECP256R1()) - payload, _ = _signed_claim(service, device_key) - current[0] += timedelta(seconds=2) - - with pytest.raises(PairingSecretError, match="已过期"): - service.claim(payload) - storage.close() diff --git a/tests/mobile_realtime/test_plugin_ui_scheduler.py b/tests/mobile_realtime/test_plugin_ui_scheduler.py deleted file mode 100644 index 7735f70ef..000000000 --- a/tests/mobile_realtime/test_plugin_ui_scheduler.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import annotations - -import asyncio -from collections.abc import Callable - -import pytest - -from infra.mobile_realtime.plugin_ui import PluginUiQuery, PluginUiQueryScheduler - - -class _BlockingProvider: - def __init__(self) -> None: - self.release = asyncio.Event() - self.started = asyncio.Event() - self.active = 0 - self.max_active = 0 - self.calls: list[str] = [] - - def catalog(self) -> dict[str, object]: - raise AssertionError("调度测试不应读取插件目录") - - def asset( - self, - plugin_id: str, - plugin_revision: str, - kind: str, - sha256: str, - ) -> dict[str, object]: - raise AssertionError("调度测试不应读取插件资源") - - async def query( - self, - plugin_id: str, - plugin_revision: str, - method: str, - payload: dict[str, object], - *, - session_id: str | None, - turn_id: str | None, - ) -> dict[str, object]: - self.calls.append(plugin_id) - self.active += 1 - self.max_active = max(self.max_active, self.active) - self.started.set() - try: - await self.release.wait() - return {"plugin_id": plugin_id} - finally: - self.active -= 1 - - -def _query(index: int, *, plugin_id: str, slot: str, owner: str = "owner") -> PluginUiQuery: - return PluginUiQuery( - request_id=f"request-{index}", - owner_id=owner, - plugin_id=plugin_id, - plugin_revision="revision-1", - method="read.current", - payload={}, - slot=slot, - session_id=None, - turn_id=None, - ) - - -async def _wait_until(predicate: Callable[[], bool]) -> None: - for _ in range(100): - if predicate(): - return - await asyncio.sleep(0.005) - raise AssertionError("等待调度状态超时") - - -@pytest.mark.asyncio -async def test_interactive_queries_use_two_reserved_device_slots() -> None: - provider = _BlockingProvider() - scheduler = PluginUiQueryScheduler(provider) - regular = [ - asyncio.create_task( - scheduler.execute( - "device", - _query(index, plugin_id=f"plugin-{index}", slot="turn.after_answer"), - ) - ) - for index in range(2) - ] - await _wait_until(lambda: provider.active == 2) - - dashboard = asyncio.create_task( - scheduler.execute( - "device", - _query(2, plugin_id="dashboard", slot="dashboard.main"), - ) - ) - drawer = asyncio.create_task( - scheduler.execute( - "device", - _query(3, plugin_id="drawer", slot="drawer.panel"), - ) - ) - await _wait_until(lambda: provider.active == 4) - - provider.release.set() - await asyncio.gather(*regular, dashboard, drawer) - assert provider.max_active == 4 - - -@pytest.mark.asyncio -async def test_plugin_gate_caps_same_plugin_at_two_queries() -> None: - provider = _BlockingProvider() - scheduler = PluginUiQueryScheduler(provider) - tasks = [ - asyncio.create_task( - scheduler.execute( - "device", - _query(index, plugin_id="same", slot="dashboard.main"), - ) - ) - for index in range(3) - ] - - await _wait_until(lambda: provider.active == 2) - await asyncio.sleep(0.02) - assert provider.active == 2 - assert len(provider.calls) == 2 - - provider.release.set() - await asyncio.gather(*tasks) - assert len(provider.calls) == 3 - - -@pytest.mark.asyncio -async def test_cancel_owner_cancels_only_owned_queries() -> None: - provider = _BlockingProvider() - scheduler = PluginUiQueryScheduler(provider) - cancelled = asyncio.create_task( - scheduler.execute( - "device", - _query(1, plugin_id="first", slot="dashboard.main", owner="old"), - ) - ) - survivor = asyncio.create_task( - scheduler.execute( - "device", - _query(2, plugin_id="second", slot="dashboard.main", owner="new"), - ) - ) - await _wait_until(lambda: provider.active == 2) - - assert await scheduler.cancel_owner("device", "old") == 1 - with pytest.raises(asyncio.CancelledError): - await cancelled - assert not survivor.done() - - provider.release.set() - assert await survivor == {"plugin_id": "second"} diff --git a/tests/mobile_webui/test_auto_publish.py b/tests/mobile_webui/test_auto_publish.py deleted file mode 100644 index 52cd8a68c..000000000 --- a/tests/mobile_webui/test_auto_publish.py +++ /dev/null @@ -1,160 +0,0 @@ -from __future__ import annotations - -import subprocess -from pathlib import Path - -import pytest - -from infra.mobile_webui.auto_publish import auto_publish_webui -from infra.mobile_webui.manifest import manifest_from_directory -from infra.mobile_webui.store import MobileWebUiStore - - -def _run(repository: Path, *args: str) -> str: - return subprocess.run( - ["git", *args], - cwd=repository, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - - -def _repository(tmp_path: Path, *, branch: str = "main") -> Path: - repository = tmp_path / "repository" - repository.mkdir() - _run(repository, "init", "-q", "-b", branch) - _run(repository, "config", "user.email", "test@example.invalid") - _run(repository, "config", "user.name", "Test") - (repository / "tracked.txt").write_text("source\n", encoding="utf-8") - (repository / "scripts").mkdir() - (repository / "scripts/publish-mobile-webui.py").write_text("pass\n", encoding="utf-8") - _run(repository, "add", ".") - _run(repository, "commit", "-qm", "initial") - head = _run(repository, "rev-parse", "HEAD") - _run(repository, "update-ref", "refs/remotes/origin/main", head) - return repository - - -def _advance_tracked_main(repository: Path) -> None: - """推进 origin/main,同时保持本地 main 停在原提交。""" - - # 1. 在临时分支创建远端后继提交 - _run(repository, "switch", "-qc", "remote-main") - (repository / "tracked.txt").write_text("remote\n", encoding="utf-8") - _run(repository, "commit", "-am", "remote", "-q") - remote_head = _run(repository, "rev-parse", "HEAD") - - # 2. 只推进 remote-tracking ref,再回到旧 main - _run(repository, "update-ref", "refs/remotes/origin/main", remote_head) - _run(repository, "switch", "-q", "main") - - -def test_feature_branch_does_not_reconcile(tmp_path: Path) -> None: - repository = _repository(tmp_path, branch="feature") - assert not auto_publish_webui( - repository, - tmp_path / "workspace", - server_id="server-1", - ) - - -def test_unsynchronized_main_fails_loud(tmp_path: Path) -> None: - repository = _repository(tmp_path) - _advance_tracked_main(repository) - with pytest.raises(RuntimeError, match="拒绝未同步的 main"): - auto_publish_webui( - repository, - tmp_path / "workspace", - server_id="server-1", - ) - - -@pytest.mark.parametrize("stable_matches_head", [True, False]) -def test_unsynchronized_main_with_existing_stable( - tmp_path: Path, - stable_matches_head: bool, -) -> None: - repository = _repository(tmp_path) - workspace = tmp_path / "workspace" - head = _run(repository, "rev-parse", "HEAD") - build = tmp_path / "stable-build" - build.mkdir() - (build / "mobile.html").write_text("stable\n", encoding="utf-8") - manifest, contents = manifest_from_directory( - build, - source_repository=str(repository), - source_commit=head if stable_matches_head else "e" * 40, - source_tree=_run(repository, "rev-parse", "HEAD^{tree}"), - input_digest="a" * 64, - build_context_digest="b" * 64, - dirty_provenance=None, - reproducible=True, - builder_identity={ - "node_version": "v22.23.1", - "npm_version": "10.9.0", - "package_lock_digest": "c" * 64, - "build_script_digest": "d" * 64, - }, - ) - store = MobileWebUiStore(workspace / "mobile-webui", server_id="server-1") - before = store.publish(manifest, contents, stable=True, preview=False) - store.close() - _advance_tracked_main(repository) - - if stable_matches_head: - assert not auto_publish_webui(repository, workspace, server_id="server-1") - else: - with pytest.raises(RuntimeError, match="拒绝未同步的 main"): - auto_publish_webui(repository, workspace, server_id="server-1") - - store = MobileWebUiStore(workspace / "mobile-webui", server_id="server-1") - try: - assert store.get_release() == before - assert store._db.execute( - "SELECT COUNT(*) FROM webui_publication_journal" - ).fetchone()[0] == 1 - finally: - store.close() - - -def test_dirty_main_does_not_reconcile(tmp_path: Path) -> None: - repository = _repository(tmp_path) - (repository / "tracked.txt").write_text("dirty\n", encoding="utf-8") - - assert not auto_publish_webui( - repository, - tmp_path / "workspace", - server_id="server-1", - ) - - -def test_new_main_invokes_stable_publisher(tmp_path: Path) -> None: - repository = _repository(tmp_path) - - assert auto_publish_webui( - repository, - tmp_path / "workspace", - server_id="server-1", - ) - - -def test_applied_main_is_a_noop(tmp_path: Path) -> None: - repository = _repository(tmp_path) - head = _run(repository, "rev-parse", "HEAD") - store = MobileWebUiStore(tmp_path / "workspace/mobile-webui", server_id="server-1") - store._db.execute( - "INSERT INTO webui_generations(generation_id, target_key, manifest_digest, manifest_json, created_at, source_repository, source_commit, source_tree) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - ("a" * 64, "b" * 64, "c" * 64, b"{}", "2026-08-08T00:00:00Z", "repo", head, "d" * 40), - ) - store._db.execute( - "INSERT INTO webui_publication_journal(sequence, generation_id, operation, release_epoch, stable, preview, actor, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - (1, "a" * 64, "publish", store._release_epoch(), 1, 0, "test", "2026-08-08T00:00:00Z"), - ) - store.close() - - assert not auto_publish_webui( - repository, - tmp_path / "workspace", - server_id="server-1", - ) diff --git a/tests/mobile_webui/test_gateway_http.py b/tests/mobile_webui/test_gateway_http.py deleted file mode 100644 index b0605552c..000000000 --- a/tests/mobile_webui/test_gateway_http.py +++ /dev/null @@ -1,267 +0,0 @@ -from __future__ import annotations - -import asyncio -import base64 -from collections import deque -from dataclasses import replace -from datetime import datetime, timezone -from pathlib import Path -from typing import cast -from uuid import uuid4 - -from cryptography.hazmat.primitives.asymmetric import ec -from fastapi import WebSocket -import httpx -import pytest - -from agent.config_models import MobileRealtimeConfig -from infra.mobile_realtime.gateway import ( - ActiveMobileConnection, - MobileGatewayRuntime, - MobileWebUiHttpError, - create_mobile_gateway_app, -) -from infra.mobile_realtime.storage import DeviceRecord -from infra.mobile_webui.manifest import manifest_from_directory -from infra.mobile_webui.store import MobileWebUiStore - - -_SOURCE = { - "source_repository": "https://github.com/example/repo", - "source_commit": "a" * 40, - "source_tree": "b" * 40, - "input_digest": "c" * 64, - "build_context_digest": "d" * 64, - "dirty_provenance": None, - "reproducible": True, - "builder_identity": { - "node_version": "v22.23.1", - "npm_version": "10.9.0", - "package_lock_digest": "e" * 64, - "build_script_digest": "f" * 64, - }, -} - - -def _websocket_stub() -> WebSocket: - return cast(WebSocket, object()) - - -class _FakeKeyset: - class Manifest: - server_id = "server-1" - - manifest = Manifest() - identity_private_key = ec.generate_private_key(ec.SECP256R1()) - - -class _FakeStorage: - def __init__(self, device: DeviceRecord) -> None: - self.device = device - - def read_device(self, device_id: str) -> DeviceRecord | None: - return self.device if device_id == self.device.device_id else None - - -def _runtime(tmp_path: Path) -> tuple[MobileGatewayRuntime, MobileWebUiStore, _FakeStorage, str, str]: - build = tmp_path / "build" - build.mkdir() - (build / "mobile.html").write_bytes(b"mobile") - manifest, contents = manifest_from_directory(build, **_SOURCE) - store = MobileWebUiStore(tmp_path / "publication", server_id="server-1") - release = store.publish(manifest, contents, stable=True, preview=False) - device = DeviceRecord( - "device-1", - "pub", - "Pixel", - datetime.now(timezone.utc), - None, - ("mobile-webui-ota-v1",), - ) - storage = _FakeStorage(device) - runtime = MobileGatewayRuntime( - config=MobileRealtimeConfig(), - storage=storage, # type: ignore[arg-type] - pairing=object(), # type: ignore[arg-type] - authenticator=object(), # type: ignore[arg-type] - inbox=object(), # type: ignore[arg-type] - approvals=object(), # type: ignore[arg-type] - keyset=_FakeKeyset(), # type: ignore[arg-type] - publication=store, - ) - runtime._connections[device.device_id] = ActiveMobileConnection( - _websocket_stub(), - 7, - asyncio.Lock(), - deque(), - True, - None, - device.capabilities, - ) - assert release.stable is not None - grant = runtime.webui_http_tickets.issue( - device_id=device.device_id, - connection_epoch=7, - release=release, - target_key=release.stable.target_key, - ) - return runtime, store, storage, grant.ticket, release.stable.manifest_digest - - -@pytest.mark.asyncio -async def test_webui_http_headers_ranges_and_ticket_lifecycle(tmp_path: Path) -> None: - runtime, store, storage, ticket, manifest_digest = _runtime(tmp_path) - app = create_mobile_gateway_app(runtime) - headers = {"Authorization": f"Bearer {ticket}"} - runtime.start() - try: - transport = httpx.ASGITransport(app=app) - async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client: - manifest_response = await client.get(f"/mobile/webui/v1/manifest/{manifest_digest}", headers=headers) - assert manifest_response.status_code == 200 - assert manifest_response.headers["cache-control"] == "no-store, no-transform" - assert manifest_response.headers["etag"] == f'"{manifest_digest}"' - assert manifest_response.headers["content-digest"].startswith("sha-256=:") - assert manifest_response.headers["repr-digest"] == f"sha-256=:{base64.b64encode(bytes.fromhex(manifest_digest)).decode()}:" - - manifest = manifest_response.json() - blob_digest = manifest["files"][0]["sha256"] - blob_response = await client.get(f"/mobile/webui/v1/blob/{blob_digest}", headers=headers) - assert blob_response.status_code == 200 - assert blob_response.headers["cache-control"] == "private, max-age=31536000, immutable, no-transform" - assert blob_response.headers["etag"] == f'"{blob_digest}"' - assert "content-range" not in blob_response.headers - ranged = await client.get( - f"/mobile/webui/v1/blob/{blob_digest}", - headers={**headers, "Range": "bytes=0-3", "If-Range": f'"{blob_digest}"'}, - ) - assert ranged.status_code == 206 - assert ranged.headers["content-range"] == f"bytes 0-3/{len(blob_response.content)}" - assert ranged.headers["repr-digest"] == f"sha-256=:{base64.b64encode(bytes.fromhex(blob_digest)).decode()}:" - invalid_range = await client.get( - f"/mobile/webui/v1/blob/{blob_digest}", - headers={**headers, "Range": "bytes=999-1000"}, - ) - assert invalid_range.status_code == 416 - assert invalid_range.json()["error"]["code"] == "invalid_range" - not_member = await client.get(f"/mobile/webui/v1/blob/{'0' * 64}", headers=headers) - assert not_member.status_code == 404 - assert not_member.json()["error"]["code"] == "resource_not_found" - - runtime._connections["device-1"] = ActiveMobileConnection( - _websocket_stub(), - 8, - asyncio.Lock(), - deque(), - True, - None, - ("mobile-webui-ota-v1",), - ) - stale_epoch = await client.get(f"/mobile/webui/v1/manifest/{manifest_digest}", headers=headers) - assert stale_epoch.status_code == 401 - assert stale_epoch.json()["error"]["code"] == "invalid_ticket" - - runtime._connections["device-1"] = ActiveMobileConnection( - _websocket_stub(), - 7, - asyncio.Lock(), - deque(), - True, - None, - ("mobile-webui-ota-v1",), - ) - fresh_release = store.get_release() - assert fresh_release.stable is not None - fresh_grant = runtime.webui_http_tickets.issue( - device_id="device-1", - connection_epoch=7, - release=fresh_release, - target_key=fresh_release.stable.target_key, - ) - second_build = tmp_path / "second-build" - second_build.mkdir() - (second_build / "mobile.html").write_bytes(b"new") - second_manifest, second_contents = manifest_from_directory(second_build, **_SOURCE) - store.publish(second_manifest, second_contents, preview=True) - changed = await client.get( - f"/mobile/webui/v1/manifest/{manifest_digest}", - headers={"Authorization": f"Bearer {fresh_grant.ticket}"}, - ) - assert changed.status_code == 409 - assert changed.json()["error"]["code"] == "target_changed" - storage.device = replace(storage.device, revoked_at=datetime.now(timezone.utc)) - revoked = await client.get(f"/mobile/webui/v1/manifest/{manifest_digest}", headers=headers) - assert revoked.status_code == 401 - assert revoked.json()["error"]["code"] == "invalid_ticket" - finally: - await runtime.stop() - store.close() - - -@pytest.mark.asyncio -async def test_blob_http_rechecks_selection_after_body_read(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - runtime, store, _storage, ticket, manifest_digest = _runtime(tmp_path) - runtime.start() - try: - manifest = store.get_manifest(manifest_digest) - blob_digest = manifest.files[0].sha256 - second_build = tmp_path / "race-build" - second_build.mkdir() - (second_build / "mobile.html").write_bytes(b"race") - second_manifest, second_contents = manifest_from_directory(second_build, **_SOURCE) - original_read_bytes = Path.read_bytes - reads = 0 - - def hooked_read_bytes(path: Path) -> bytes: - nonlocal reads - data = original_read_bytes(path) - if path == store.blob_path(blob_digest): - reads += 1 - if reads == 2: - store.publish(second_manifest, second_contents, preview=True) - return data - - monkeypatch.setattr(Path, "read_bytes", hooked_read_bytes) - with pytest.raises(MobileWebUiHttpError, match="release 已变化") as error: - runtime.read_webui_blob_http( - ticket=ticket, - blob_digest=blob_digest, - range_header=None, - if_range=None, - ) - assert error.value.code == "target_changed" - assert reads >= 2 - finally: - await runtime.stop() - store.close() - - -@pytest.mark.asyncio -async def test_manifest_http_rechecks_release_epoch_after_body_read(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - runtime, store, _storage, ticket, manifest_digest = _runtime(tmp_path) - runtime.start() - try: - original_get_manifest = store.get_manifest - switched = False - - def hooked_get_manifest(digest: str): - nonlocal switched - manifest = original_get_manifest(digest) - if not switched: - switched = True - next_epoch = str(uuid4()) - store._db.execute("UPDATE webui_meta SET value = ? WHERE key = 'release_epoch'", (next_epoch,)) - store._db.execute( - "UPDATE webui_release_state SET release_epoch = ? WHERE singleton = 1", - (next_epoch,), - ) - return manifest - - monkeypatch.setattr(store, "get_manifest", hooked_get_manifest) - with pytest.raises(MobileWebUiHttpError, match="release 已变化") as error: - runtime.read_webui_manifest_http(ticket=ticket, manifest_digest=manifest_digest) - assert error.value.code == "target_changed" - assert switched - finally: - await runtime.stop() - store.close() diff --git a/tests/mobile_webui/test_gateway_runtime.py b/tests/mobile_webui/test_gateway_runtime.py deleted file mode 100644 index a7db738c8..000000000 --- a/tests/mobile_webui/test_gateway_runtime.py +++ /dev/null @@ -1,237 +0,0 @@ -from __future__ import annotations - -import asyncio -from collections import deque -from dataclasses import replace -from datetime import datetime, timezone -from pathlib import Path -from typing import cast - -from fastapi import WebSocket -import pytest -from pydantic import ValidationError - -from infra.mobile_realtime.gateway import ActiveMobileConnection, MobileGatewayRuntime -from infra.mobile_realtime.storage import DeviceRecord -from infra.mobile_realtime.protocol import parse_frame -from infra.mobile_webui.manifest import WebUiManifest, manifest_from_directory -from infra.mobile_webui.store import MobileWebUiStore - - -_SOURCE = { - "source_repository": "https://github.com/example/repo", - "source_commit": "a" * 40, - "source_tree": "b" * 40, - "input_digest": "c" * 64, - "build_context_digest": "d" * 64, - "dirty_provenance": None, - "reproducible": True, - "builder_identity": { - "node_version": "v22.23.1", - "npm_version": "10.9.0", - "package_lock_digest": "e" * 64, - "build_script_digest": "f" * 64, - }, -} - - -def _manifest(root: Path, text: bytes) -> tuple[WebUiManifest, dict[str, bytes]]: - root.mkdir(parents=True, exist_ok=True) - path = root / "mobile.html" - path.write_bytes(text) - manifest, contents = manifest_from_directory(root, **_SOURCE) - return manifest, contents - - -def _websocket_stub(value: object) -> WebSocket: - return cast(WebSocket, value) - - -def _watcher_runtime(store: MobileWebUiStore) -> MobileGatewayRuntime: - runtime = object.__new__(MobileGatewayRuntime) - runtime.publication = store - runtime._publication_monitor_task = None - runtime._publication_selection_digest = store.get_release(verify_integrity=False).selection_digest - runtime._connections = {} - runtime._delivery_lock = asyncio.Lock() - return runtime - - -@pytest.mark.asyncio -async def test_publication_watcher_deduplicates_and_filters_capability(tmp_path: Path) -> None: - first, first_contents = _manifest(tmp_path / "first", b"first") - second, second_contents = _manifest(tmp_path / "second", b"second") - third, third_contents = _manifest(tmp_path / "third", b"third") - store = MobileWebUiStore(tmp_path / "store", server_id="server-1") - runtime = None - try: - store.publish(first, first_contents, stable=True, preview=False) - runtime = _watcher_runtime(store) - sent: list[tuple[str, str, str, int]] = [] - - async def send_control(*, control_type: str, payload: dict[str, object], device_id: str, connection_epoch: int) -> None: - sent.append((control_type, str(payload["selection_digest"]), device_id, connection_epoch)) - - runtime.publish_connection_control = send_control - runtime._connections = { - "enabled": ActiveMobileConnection(_websocket_stub(object()), 7, asyncio.Lock(), deque(), True, None, ("mobile-webui-ota-v1",)), - "disabled": ActiveMobileConnection(_websocket_stub(object()), 8, asyncio.Lock(), deque(), True, None, ("other",)), - "not-ready": ActiveMobileConnection(_websocket_stub(object()), 9, asyncio.Lock(), deque(), False, None, ("mobile-webui-ota-v1",)), - } - runtime.start() - runtime.start() - await asyncio.sleep(0.05) - assert sent == [] - - second_release = store.publish(second, second_contents, preview=True) - await asyncio.sleep(0.65) - assert sent == [("mobile.webui.release.changed", second_release.selection_digest, "enabled", 7)] - await asyncio.sleep(0.6) - assert len(sent) == 1 - - store.publish(second, second_contents, preview=True) - await asyncio.sleep(0.6) - assert len(sent) == 1 - - third_release = store.publish(third, third_contents, preview=True) - await asyncio.sleep(0.6) - assert sent[-1] == ("mobile.webui.release.changed", third_release.selection_digest, "enabled", 7) - assert len(sent) == 2 - finally: - if runtime is not None: - await runtime.stop() - store.close() - - -@pytest.mark.asyncio -async def test_control_delivery_rejects_replaced_epoch_and_evicts_failed_socket(tmp_path: Path) -> None: - manifest, contents = _manifest(tmp_path / "build", b"content") - store = MobileWebUiStore(tmp_path / "store", server_id="server-1") - runtime = None - try: - store.publish(manifest, contents, stable=True, preview=False) - runtime = _watcher_runtime(store) - - class Socket: - def __init__(self, *, fail: bool = False) -> None: - self.fail = fail - self.sent: list[str] = [] - self.closed = False - - async def send_text(self, value: str) -> None: - if self.fail: - raise RuntimeError("closed") - self.sent.append(value) - - async def close(self, **_kwargs: object) -> None: - self.closed = True - - old_socket = Socket() - old = ActiveMobileConnection(_websocket_stub(old_socket), 7, asyncio.Lock(), deque(), True, None, ("mobile-webui-ota-v1",)) - replacement = ActiveMobileConnection(_websocket_stub(Socket()), 8, asyncio.Lock(), deque(), True, None, ("mobile-webui-ota-v1",)) - runtime._connections = {"device": old} - runtime._connections["device"] = replacement - await runtime.publish_connection_control( - control_type="mobile.webui.release.changed", - payload={"server_id": "server-1", "selection_digest": "a" * 64}, - device_id="device", - connection_epoch=7, - ) - assert old_socket.sent == [] - - failing_socket = Socket(fail=True) - failing = ActiveMobileConnection(_websocket_stub(failing_socket), 9, asyncio.Lock(), deque(), True, None, ("mobile-webui-ota-v1",)) - runtime._connections["device"] = failing - await runtime.publish_connection_control( - control_type="mobile.webui.release.changed", - payload={"server_id": "server-1", "selection_digest": "b" * 64}, - device_id="device", - connection_epoch=9, - ) - await asyncio.sleep(0) - assert "device" not in runtime._connections - assert failing_socket.closed - finally: - if runtime is not None: - await runtime.stop() - store.close() - - -@pytest.mark.asyncio -async def test_revoke_device_commits_offline_and_notifies_active_connection() -> None: - device = DeviceRecord("device", "pub", "Pixel", datetime.now(timezone.utc), None, ()) - - class Storage: - def __init__(self) -> None: - self.device = device - - def revoke_device(self, device_id: str, *, revoked_at: datetime) -> DeviceRecord: - assert device_id == self.device.device_id - if self.device.revoked_at is None: - self.device = replace(self.device, revoked_at=revoked_at) - return self.device - - class Socket: - def __init__(self, *, fail: bool = False) -> None: - self.fail = fail - self.sent: list[str] = [] - self.closed: list[dict[str, object]] = [] - - async def send_text(self, value: str) -> None: - if self.fail: - raise RuntimeError("closed") - self.sent.append(value) - - async def close(self, **kwargs: object) -> None: - self.closed.append(kwargs) - - runtime = object.__new__(MobileGatewayRuntime) - runtime.storage = Storage() - runtime._delivery_lock = asyncio.Lock() - socket = Socket() - runtime._connections = { - device.device_id: ActiveMobileConnection(_websocket_stub(socket), 7, asyncio.Lock(), deque(), True, None, ()) - } - revoked = await runtime.revoke_device(device.device_id) - assert revoked.revoked_at is not None - assert runtime.storage.device.revoked_at == revoked.revoked_at - assert device.device_id not in runtime._connections - assert socket.closed == [{"code": 4403, "reason": "设备已撤销"}] - frame = parse_frame(socket.sent[0]) - assert frame.kind == "control" - assert frame.type == "device.revoked" - assert frame.connection_epoch == 7 - - offline = await runtime.revoke_device("device") - assert offline.revoked_at == revoked.revoked_at - - failing_socket = Socket(fail=True) - runtime._connections[device.device_id] = ActiveMobileConnection( - _websocket_stub(failing_socket), - 8, - asyncio.Lock(), - deque(), - True, - None, - (), - ) - failed = await runtime.revoke_device(device.device_id) - assert failed.revoked_at == revoked.revoked_at - assert device.device_id not in runtime._connections - assert failing_socket.closed == [{"code": 4403, "reason": "设备已撤销"}] - - -def test_device_revoked_cannot_enter_durable_event_enqueue() -> None: - runtime = object.__new__(MobileGatewayRuntime) - - class Inbox: - def enqueue(self, **_kwargs: object) -> None: - raise AssertionError("invalid durable event must fail before inbox write") - - runtime.inbox = Inbox() - with pytest.raises(ValidationError): - runtime._enqueue_event( - device_id="device", - event_type="device.revoked", - payload={"device_id": "device"}, - ) diff --git a/tests/mobile_webui/test_publisher.py b/tests/mobile_webui/test_publisher.py deleted file mode 100644 index e22ac814f..000000000 --- a/tests/mobile_webui/test_publisher.py +++ /dev/null @@ -1,279 +0,0 @@ -from __future__ import annotations - -import importlib.util -import json -import os -import subprocess -from pathlib import Path - -import pytest -from infra.mobile_webui.store import MobileWebUiStore - - -_PUBLISHER_PATH = Path(__file__).parents[2] / "scripts" / "publish-mobile-webui.py" -_SPEC = importlib.util.spec_from_file_location("publish_mobile_webui", _PUBLISHER_PATH) -assert _SPEC is not None and _SPEC.loader is not None -_PUBLISHER = importlib.util.module_from_spec(_SPEC) -_SPEC.loader.exec_module(_PUBLISHER) - - -def _run(repo: Path, *args: str) -> None: - subprocess.run(["git", *args], cwd=repo, check=True, capture_output=True) - - -def _repo(tmp_path: Path, *, lock: bool = True) -> Path: - repo = tmp_path / "repo" - (repo / "frontend/chat").mkdir(parents=True) - (repo / "scripts").mkdir() - (repo / "frontend/chat/mobile.html").write_text("base", encoding="utf-8") - (repo / "package.json").write_text( - json.dumps( - { - "name": "test", - "scripts": { - "build:mobile-web": ( - "node -e \"const fs=require('fs');" - "fs.mkdirSync(process.env.AKASHIC_MOBILE_WEB_OUT_DIR,{recursive:true});" - "fs.copyFileSync('frontend/chat/mobile.html'," - "process.env.AKASHIC_MOBILE_WEB_OUT_DIR+'/mobile.html')\"" - ) - }, - } - ) - + "\n", - encoding="utf-8", - ) - (repo / "scripts/package-mobile-web.sh").write_text("#!/bin/sh\n", encoding="utf-8") - if lock: - (repo / "package-lock.json").write_text( - '{"name":"test","lockfileVersion":3,"packages":{"":{"name":"test"}}}\n', - encoding="utf-8", - ) - _run(repo, "init", "-q") - _run(repo, "config", "user.email", "test@example.invalid") - _run(repo, "config", "user.name", "Test") - _run(repo, "add", ".") - _run(repo, "commit", "-qm", "initial") - _run(repo, "remote", "add", "origin", "https://github.com/example/test.git") - return repo - - -def test_stable_without_commit_lock_fails_before_build(tmp_path: Path) -> None: - repo = _repo(tmp_path, lock=False) - with pytest.raises(RuntimeError, match="package-lock"): - _PUBLISHER._build(repo, None, allow_dirty=False, source_commit=None, stable=True) - - -def test_dirty_preview_overlay_is_frozen_with_untracked_inputs(tmp_path: Path) -> None: - repo = _repo(tmp_path) - (repo / "frontend/chat/mobile.html").write_text("dirty", encoding="utf-8") - (repo / "frontend/chat/extra.js").write_text("extra", encoding="utf-8") - (repo / "frontend/theme").mkdir() - (repo / "frontend/theme/shared.ts").write_text("export const theme = 'light';\n", encoding="utf-8") - (repo / ".gitignore").write_text("frontend/chat/ignored.js\n", encoding="utf-8") - _run(repo, "add", ".gitignore") - _run(repo, "commit", "-qm", "ignore rule") - (repo / "frontend/chat/ignored.js").write_text("ignored", encoding="utf-8") - commit = _PUBLISHER._git(repo, "rev-parse", "HEAD") - with _PUBLISHER._build_source(repo, commit, dirty=True) as snapshot: - assert (snapshot / "frontend/chat/mobile.html").read_text(encoding="utf-8") == "dirty" - assert (snapshot / "frontend/chat/extra.js").read_text(encoding="utf-8") == "extra" - assert (snapshot / "frontend/theme/shared.ts").read_text(encoding="utf-8") == "export const theme = 'light';\n" - assert not (snapshot / "frontend/chat/ignored.js").exists() - (repo / "frontend/chat/mobile.html").write_text("changed-after-freeze", encoding="utf-8") - assert (snapshot / "frontend/chat/mobile.html").read_text(encoding="utf-8") == "dirty" - - -def test_dirty_overlay_rejects_symlink_and_preserves_tracked_deletion(tmp_path: Path) -> None: - repo = _repo(tmp_path) - (repo / "frontend/chat/mobile.html").unlink() - commit = _PUBLISHER._git(repo, "rev-parse", "HEAD") - with _PUBLISHER._build_source(repo, commit, dirty=True) as snapshot: - assert not (snapshot / "frontend/chat/mobile.html").exists() - (repo / "frontend/chat/link.js").symlink_to(repo / "package.json") - with pytest.raises(RuntimeError, match="symlink"): - with _PUBLISHER._build_source(repo, commit, dirty=True): - pass - - -def test_clean_sidecar_uses_frozen_source_snapshot(tmp_path: Path) -> None: - repo = _repo(tmp_path) - output = tmp_path / "output" - output.mkdir() - environment = _PUBLISHER._build_environment(output) - before = _PUBLISHER._capture_provenance(repo, environment=environment, output_dir=output) - (output / "mobile.html").write_text("base", encoding="utf-8") - (tmp_path / "output.provenance.json").write_text( - json.dumps({**before, "artifact_digest": _PUBLISHER._artifact_digest(output)}), - encoding="utf-8", - ) - before_manifest, _ = _PUBLISHER._manifest(repo, output, allow_dirty=False) - (repo / "frontend/chat/mobile.html").write_text("changed", encoding="utf-8") - after_manifest, _ = _PUBLISHER._manifest(repo, output, allow_dirty=False) - assert after_manifest.generation_id == before_manifest.generation_id - - -def test_build_context_tracks_effective_env_and_normalizes_output_dir(tmp_path: Path) -> None: - repo = _repo(tmp_path) - environment = os.environ.copy() - environment["VITE_PUBLIC_THEME"] = "dark" - environment["VITE_PRIVATE_TOKEN"] = "do-not-write-this-value" - first = _PUBLISHER._capture_provenance( - repo, - environment={**environment, "AKASHIC_MOBILE_WEB_OUT_DIR": "/tmp/mobile-web-a"}, - output_dir=Path("/tmp/mobile-web-a"), - ) - second = _PUBLISHER._capture_provenance( - repo, - environment={**environment, "AKASHIC_MOBILE_WEB_OUT_DIR": "/tmp/mobile-web-b"}, - output_dir=Path("/tmp/mobile-web-b"), - ) - assert first["build_context_digest"] == second["build_context_digest"] - assert "do-not-write-this-value" not in repr(first) - - changed = _PUBLISHER._capture_provenance( - repo, - environment={ - **environment, - "VITE_PUBLIC_THEME": "light", - "AKASHIC_MOBILE_WEB_OUT_DIR": "/tmp/mobile-web-b", - }, - output_dir=Path("/tmp/mobile-web-b"), - ) - assert changed["build_context_digest"] != second["build_context_digest"] - - -def test_source_bound_provenance_ignores_inherited_pwd(tmp_path: Path) -> None: - repo = _repo(tmp_path) - environment = os.environ.copy() - first = _PUBLISHER._capture_provenance( - repo, - environment={**environment, "PWD": str(tmp_path / "inherited-a")}, - output_dir=tmp_path / "output", - ) - second = _PUBLISHER._capture_provenance( - repo, - environment={**environment, "PWD": str(tmp_path / "inherited-b")}, - output_dir=tmp_path / "output", - ) - assert first["build_context_digest"] == second["build_context_digest"] - - -def test_run_build_binds_pwd_to_build_workspace(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - repo = _repo(tmp_path) - calls: list[tuple[list[str], Path, dict[str, str]]] = [] - - def fake_run( - command: list[str], *, cwd: Path, env: dict[str, str], check: bool - ) -> None: - assert check is True - calls.append((command, cwd, env)) - - monkeypatch.setattr(_PUBLISHER.subprocess, "run", fake_run) - _PUBLISHER._run_build( - repo, - environment={"PATH": os.environ["PATH"], "PWD": str(tmp_path / "inherited")}, - lock_available=True, - ) - assert len(calls) == 2 - assert all(cwd == repo for _, cwd, _ in calls) - assert all(env["PWD"] == str(repo.resolve()) for _, _, env in calls) - - -def test_build_environment_is_controlled_and_clean_publish_revalidates(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - repo = _repo(tmp_path) - monkeypatch.setenv("UNTRACKED_BUILD_INPUT", "ignored") - output = tmp_path / "build-output" - built = _PUBLISHER._build(repo, output, allow_dirty=False, source_commit=None, stable=True) - assert "UNTRACKED_BUILD_INPUT" not in _PUBLISHER._build_environment(output) - manifest, contents = _PUBLISHER._manifest(repo, built, allow_dirty=False) - store = MobileWebUiStore(tmp_path / "store", server_id="server-1") - try: - release = store.publish(manifest, contents, stable=True, preview=False) - assert release.stable is not None - assert release.stable.generation_id == manifest.generation_id - finally: - store.close() - - monkeypatch.setenv("VITE_PUBLIC_THEME", "light") - changed_environment = _PUBLISHER._build_environment(output) - original_environment = dict(changed_environment) - original_environment.pop("VITE_PUBLIC_THEME", None) - first = _PUBLISHER._capture_provenance( - repo, - environment=original_environment, - output_dir=output, - ) - second = _PUBLISHER._capture_provenance( - repo, - environment=changed_environment, - output_dir=output, - ) - assert first["build_context_digest"] != second["build_context_digest"] - - -def test_clean_publish_rejects_nondeterministic_artifact_rebuild(tmp_path: Path) -> None: - repo = _repo(tmp_path) - package = json.loads((repo / "package.json").read_text(encoding="utf-8")) - package["scripts"]["build:mobile-web"] = ( - "node -e \"const fs=require('fs');" - "fs.mkdirSync(process.env.AKASHIC_MOBILE_WEB_OUT_DIR,{recursive:true});" - "fs.writeFileSync(process.env.AKASHIC_MOBILE_WEB_OUT_DIR+'/mobile.html',String(Math.random()))\"" - ) - (repo / "package.json").write_text(json.dumps(package) + "\n", encoding="utf-8") - _run(repo, "add", "package.json") - _run(repo, "commit", "-qm", "nondeterministic build") - output = tmp_path / "nondeterministic-output" - built = _PUBLISHER._build(repo, output, allow_dirty=False, source_commit=None, stable=True) - with pytest.raises(RuntimeError, match="artifact 不可复现"): - _PUBLISHER._manifest(repo, built, allow_dirty=False) - - -def test_internal_publish_cleans_build_sidecar_on_success( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - capsys: pytest.CaptureFixture[str], -) -> None: - repo = _repo(tmp_path) - workspace = tmp_path / "runtime-workspace" - monkeypatch.chdir(repo) - monkeypatch.setenv("PWD", str(repo)) - assert _PUBLISHER.main( - [ - "publish", - "--source-repository", - str(repo), - "--workspace", - str(workspace), - "--server-id", - "server-1", - "--stable", - ] - ) == 0 - _ = capsys.readouterr() - assert not list(workspace.glob("mobile-webui-build-*.provenance.json")) - - -def test_internal_publish_cleans_build_sidecar_on_build_failure(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - repo = _repo(tmp_path) - workspace = tmp_path / "runtime-workspace" - - def fail_build(source: Path, output: Path, **_kwargs: object) -> Path: - output.mkdir(parents=True, exist_ok=True) - output.with_name(output.name + ".provenance.json").write_text("owned", encoding="utf-8") - raise RuntimeError("synthetic build failure") - - monkeypatch.setattr(_PUBLISHER, "_build", fail_build) - with pytest.raises(RuntimeError, match="synthetic build failure"): - _PUBLISHER.main( - [ - "publish", - "--source-repository", - str(repo), - "--workspace", - str(workspace), - "--server-id", - "server-1", - ] - ) - assert not list(workspace.glob("mobile-webui-build-*.provenance.json")) diff --git a/tests/semantic/test_gate_workspace_contract.py b/tests/semantic/test_gate_workspace_contract.py deleted file mode 100644 index 16d7854c2..000000000 --- a/tests/semantic/test_gate_workspace_contract.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -import os -import tomllib -from pathlib import Path - -import pytest - -from tests_scenarios.contracts.oracles import assert_isolated_gate_paths - - -@pytest.mark.skipif( - "AKASHIC_GATE_INVENTORY" not in os.environ, - reason="只在 change-gate Docker sandbox 中执行", -) -def test_change_gate_uses_only_fresh_sandbox_state() -> None: - sandbox = Path("/sandbox") - workspace = Path(os.environ["AKASHIC_DEBUG_WORKSPACE"]) - plugin_home = Path(os.environ["AKASHIC_PLUGIN_HOME"]) - config = Path(os.environ["AKASHIC_DEBUG_CONFIG"]) - - assert_isolated_gate_paths( - sandbox=sandbox, - workspace=workspace, - plugin_home=plugin_home, - config=config, - ) - assert Path(os.environ["HOME"]).resolve() == sandbox / "home" - assert list(workspace.iterdir()) == [] - assert list(plugin_home.iterdir()) == [] - assert not (workspace / "sessions.db").exists() - payload = tomllib.loads(config.read_text(encoding="utf-8")) - assert payload["runtime"]["workspace"] == "/sandbox/workspace" diff --git a/tests/semantic/test_recursive_plugin_self_validation_trajectory.py b/tests/semantic/test_recursive_plugin_self_validation_trajectory.py deleted file mode 100644 index ead0e8e3f..000000000 --- a/tests/semantic/test_recursive_plugin_self_validation_trajectory.py +++ /dev/null @@ -1,475 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -import subprocess -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast - -import pytest - -from agent.control.models import TurnRequest -from agent.core.passive_turn import DefaultReasoner -from agent.control.runtime import ConversationRuntime -from agent.looping.core import AgentLoop -from agent.looping.ports import ( - AgentLoopConfig, - AgentLoopDeps, - LLMConfig, -) -from agent.plugin_composition import ( - LLMResponse, - TOOL_CATALOG, - PluginToolDefinition, - ToolCall, -) -from agent.plugin_composition.channels import ChannelDeliveryReceipt -from agent.plugin_composition.channels import DeliveryStatus as ChannelDeliveryStatus -from agent.persona import reset_veda -from agent.plugins.manager import PluginManager -from agent.plugins.reload_journal import ReloadJournal -from agent.plugins.snapshot import get_current_runtime_snapshot -from agent.tools.message_push import MessagePushTool -from agent.tools.registry import ToolRegistry -from bootstrap.app import AppRuntime -from bootstrap.control_execution import execute_control_turn -from bus.event_bus import EventBus -from bus.queue import MessageBus -from session.manager import SessionManager -from tests.compaction_fakes import install_test_projection -from tests.provider_fakes import ProviderContextBudgetStub -from tests.model_plugin_fakes import ( - register_test_model_provider, - unregister_test_model_provider, -) -from tests_scenarios.contracts.oracles import ( - assert_recursive_candidate_ready, - assert_recursive_candidate_trajectory, -) - - -class _TrajectoryProvider(ProviderContextBudgetStub): - def __init__( - self, - parent_release: asyncio.Event, - *, - fake_tool_success: bool, - ) -> None: - self.parent_release = parent_release - self.fake_tool_success = fake_tool_success - self.parent_started = asyncio.Event() - self.seen: dict[str, str] = {} - - async def chat( - self, - messages: list[dict[str, Any]], - **_kwargs: Any, - ) -> LLMResponse: - """按输入驱动确定性模型,同时记录当前生产 snapshot lease。""" - - rendered = json.dumps(messages, ensure_ascii=False) - snapshot = get_current_runtime_snapshot() - snapshot_id = "" if snapshot is None else snapshot.snapshot_id - - # 1. 父 turn 持有 stable,直到测试显式完成 candidate promote。 - if "parent-hold" in rendered: - self.seen["parent_before"] = snapshot_id - self.parent_started.set() - await self.parent_release.wait() - current = get_current_runtime_snapshot() - self.seen["parent_after"] = "" if current is None else current.snapshot_id - return LLMResponse(content="parent completed") - - # 2. 普通新 turn 只能观察 stable。 - if "ordinary-stable" in rendered: - self.seen["ordinary"] = snapshot_id - return LLMResponse(content="ordinary completed") - - # 3. latest 验证必须经过真实候选工具和 message_push。 - self.seen["validation"] = snapshot_id - if self.fake_tool_success: - return LLMResponse(content="candidate validated") - tool_results = sum(message.get("role") == "tool" for message in messages) - if tool_results == 0: - return LLMResponse( - content=None, - tool_calls=[ - ToolCall( - "candidate-call", - "candidate_only_tool", - {"description": "核对候选领域状态"}, - ) - ], - ) - if tool_results == 1: - return LLMResponse( - content=None, - tool_calls=[ - ToolCall( - "push-call", - "message_push", - { - "target_channel": "proof", - "target_chat_id": "parent", - "message": "candidate checked", - "description": "发送验证回执", - }, - ) - ], - ) - return LLMResponse(content="candidate validated") - - -def _write_plugin_source(source: Path, *, forged_domain: bool) -> None: - """创建一个真实 Git v3 插件,候选工具读取 generation 数据状态。""" - - source.mkdir() - domain_expression = ( - "'forged-domain'" - if forged_domain - else "(Path(generation.data_dir) / 'domain.txt').read_text(encoding='utf-8').strip()" - ) - (source / "plugin.py").write_text( - "import json\n" - "from pathlib import Path\n" - "from agent.plugin_composition import TOOL_CATALOG, PluginToolDefinition\n" - "from agent.plugins.snapshot import get_current_runtime_snapshot\n\n" - "api_version = 3\n" - "name = 'candidate_only'\n" - "version = '1.0.0'\n" - "inject = (TOOL_CATALOG,)\n\n" - "async def candidate_only_tool(context, arguments):\n" - " del context, arguments\n" - " snapshot = get_current_runtime_snapshot()\n" - " if snapshot is None:\n" - " raise RuntimeError('candidate tool 缺少 RuntimeSnapshot')\n" - " generation = snapshot.generations.get('candidate_only@lab')\n" - " if generation is None:\n" - " raise RuntimeError('candidate tool 缺少 candidate generation')\n" - f" domain = {domain_expression}\n" - " return json.dumps({'domain': domain, 'snapshot': snapshot.snapshot_id})\n\n" - "async def apply(ctx, config):\n" - " del config\n" - " await ctx.require(TOOL_CATALOG).register(ctx, PluginToolDefinition(\n" - " name='candidate_only_tool',\n" - " description='Read the candidate domain marker.',\n" - " parameters={\n" - " 'type': 'object',\n" - " 'properties': {},\n" - " 'required': [],\n" - " 'additionalProperties': False,\n" - " },\n" - " handler_export='candidate_only_tool',\n" - " risk='read-only',\n" - " always_on=True,\n" - " ))\n", - encoding="utf-8", - ) - (source / "akashic.plugin.toml").write_text( - "schema_version = 1\n" - 'name = "candidate_only"\n' - 'version = "1.0.0"\n' - "api_version = 3\n" - 'entrypoint = "plugin.py"\n', - encoding="utf-8", - ) - subprocess.run(["git", "init", "-q"], cwd=source, check=True) - subprocess.run( - ["git", "config", "user.email", "test@example.com"], - cwd=source, - check=True, - ) - subprocess.run( - ["git", "config", "user.name", "Test"], - cwd=source, - check=True, - ) - subprocess.run(["git", "add", "."], cwd=source, check=True) - subprocess.run(["git", "commit", "-q", "-m", "candidate"], cwd=source, check=True) - - -async def _run_trajectory( - tmp_path: Path, - *, - forged_domain: bool = False, - fake_tool_success: bool = False, - validation_runtime: str = "latest", -) -> dict[str, object]: - """运行 install 到 promote 的完整生产轨迹,并只从正式状态入口取证。""" - - # 1. 创建真实 loop、SessionDB、control runtime 与 stable snapshot。 - workspace = tmp_path / "workspace" - workspace.mkdir(parents=True) - reset_veda(workspace) - (workspace / "domain.txt").write_text("domain-ready", encoding="utf-8") - candidate_data = workspace / "plugin-data" / "candidate_only-lab" - candidate_data.mkdir(parents=True) - (candidate_data / "domain.txt").write_text("domain-ready", encoding="utf-8") - builtin = tmp_path / "builtin" / "baseline" - builtin.mkdir(parents=True) - (builtin / "plugin.py").write_text( - "from tests.model_plugin_fakes import provide_test_model_services\n\n" - "api_version = 3\n" - "name = 'baseline'\n" - "version = '1.0.0'\n\n" - "async def apply(ctx, config):\n" - " del config\n" - " await provide_test_model_services(ctx)\n", - encoding="utf-8", - ) - source = tmp_path / "candidate" - _write_plugin_source(source, forged_domain=forged_domain) - bus = MessageBus() - event_bus = EventBus() - tools = ToolRegistry() - push = MessagePushTool(chat_lane=bus.chat_lane) - sequence = 0 - push_sequence = 0 - - async def deliver(_message: object, _passive: bool) -> ChannelDeliveryReceipt: - nonlocal sequence, push_sequence - sequence += 1 - push_sequence = sequence - return ChannelDeliveryReceipt( - delivery_id=f"proof-{sequence}", - status=ChannelDeliveryStatus.DELIVERED, - ) - - push.bind_v3_channel_dispatcher(deliver) - tools.register( - push, - risk="external-side-effect", - always_on=True, - source_type="builtin", - source_name="message_push", - ) - sessions = SessionManager(workspace) - parent_release = asyncio.Event() - provider = _TrajectoryProvider( - parent_release, - fake_tool_success=fake_tool_success, - ) - register_test_model_provider(workspace, provider) - - manager = PluginManager( - plugin_dirs=[builtin.parent], - event_bus=event_bus, - tool_registry=tools, - workspace=workspace, - session_manager=sessions, - installed_cache_root=tmp_path / "plugins-home" / "cache", - ) - - loop = AgentLoop( - AgentLoopDeps( - bus=bus, - tools=tools, - session_manager=sessions, - workspace=workspace, - event_bus=event_bus, - ), - AgentLoopConfig(llm=LLMConfig(max_iterations=5)), - ) - assert isinstance(loop._reasoner, DefaultReasoner) - install_test_projection(loop._reasoner) - loop.bind_runtime_snapshot_store(manager.snapshot_store) - await manager.load_all() - stable = manager.current_snapshot - assert stable is not None - - async def execute(request: TurnRequest): - return await execute_control_turn(loop, event_bus, request) - - runtime = ConversationRuntime(sessions.control_store, execute) - app = object.__new__(AppRuntime) - app.workspace = workspace - app.core = SimpleNamespace(plugin_manager=manager) - parent_handle = None - parent_lane_pending = False - try: - # 2. 父 turn 先取得 stable;install 返回后 latest 必须立即可租用。 - await bus.chat_lane.mark_passive_pending("proof", "parent") - parent_lane_pending = True - parent_handle = await runtime.start_turn( - TurnRequest( - "programmatic:parent", - "parent-hold", - { - "runtime": "stable", - "channel": "proof", - "chatId": "parent", - "inboundMetadata": { - "effects": {"post_commit": "suppress"}, - "disabled_prompt_sections": ["memory"], - }, - }, - ) - ) - await asyncio.wait_for(provider.parent_started.wait(), timeout=5) - install = await app._install_plugin(str(source), "lab", "", []) - candidate_status = cast(dict[str, object], install["candidate"]) - candidate = manager.latest_snapshot - assert candidate is not None - - # 3. 普通 stable 与显式 latest child 并发运行;child 还执行真实 push。 - ordinary_handle = await runtime.start_turn( - TurnRequest( - "programmatic:ordinary", "ordinary-stable", {"runtime": "stable"} - ) - ) - ordinary_result = await ordinary_handle.result() - assert ordinary_result.status.value == "completed" - push_history_before = sessions.control_store.fetch_session_messages( - "proof:parent" - ) - validation_handle = await runtime.start_turn( - TurnRequest( - "programmatic:validation", - "candidate-validation", - { - "runtime": validation_runtime, - "inboundMetadata": { - "effects": {"post_commit": "suppress"}, - "disabled_prompt_sections": ["memory"], - }, - }, - ) - ) - validation_result = await validation_handle.result() - parent_status_before_promote = parent_handle.record()["status"] - validation_finished_before_parent_release = ( - parent_status_before_promote == "in_progress" - ) - validation_turn = runtime.read_turn( - validation_handle.thread_id, - validation_handle.id, - ).to_dict() - validation_messages = sessions.control_store.fetch_session_messages( - validation_handle.thread_id - ) - push_history_after = sessions.control_store.fetch_session_messages( - "proof:parent" - ) - - # 4. 必须先通过候选 oracle,才允许显式 promote。 - before_promote = manager.candidate_status("candidate_only@lab") - tx_id = str(candidate_status["candidateReloadTransactionId"]) - journal = ReloadJournal(workspace) - ready_observation: dict[str, object] = { - "stable_snapshot": stable.snapshot_id, - "candidate_snapshot": candidate.snapshot_id, - "install_publication_state": install["publicationState"], - "parent_runtime": provider.seen.get("parent_before"), - "ordinary_runtime_during_validation": provider.seen.get("ordinary"), - "validation_runtime": provider.seen.get("validation"), - "validation_finished_before_parent_release": validation_finished_before_parent_release, - "parent_status_before_promote": parent_status_before_promote, - "validation_turn": validation_turn, - "candidate_tool_result": {}, - "domain_state": (workspace / "domain.txt") - .read_text(encoding="utf-8") - .strip(), - "validation_messages": validation_messages, - "push_send_sequence": push_sequence, - "push_target_history_before": push_history_before, - "push_target_history_after": push_history_after, - "stable_before_promote": before_promote["stable_snapshot_id"], - "reload_journal_events_before_promote": [ - event.phase for event in journal.events(tx_id) - ], - } - candidate_items = [ - item - for item in validation_result.items - if item.kind.value == "toolCall" - and item.data.get("name") == "candidate_only_tool" - ] - if candidate_items: - preview = candidate_items[0].data.get("resultPreview") - if isinstance(preview, str): - try: - ready_observation["candidate_tool_result"] = json.loads(preview) - except json.JSONDecodeError: - ready_observation["candidate_tool_result"] = {"raw": preview} - assert_recursive_candidate_ready(ready_observation) - - # 5. 晋升先封住 stable admission,再等待父 lease 归还。 - promotion = asyncio.create_task(app._promote_plugin("candidate_only@lab")) - while stable.accepting_leases: - await asyncio.sleep(0) - assert not promotion.done() - parent_release.set() - parent_result = await parent_handle.result() - promoted = await promotion - await manager.snapshot_store.retry_drains() - await bus.chat_lane.mark_passive_done("proof", "parent") - parent_lane_pending = False - sequence += 1 - parent_terminal_sequence = sequence - return { - **ready_observation, - "parent_terminal_status": parent_result.status.value, - "parent_terminal_sequence": parent_terminal_sequence, - "reload_journal_events_after_promote": [ - event.phase for event in journal.events(tx_id) - ], - "stable_after_promote": promoted["stable_snapshot_id"], - "parent_runtime_after_promote": provider.seen.get("parent_after"), - } - finally: - parent_release.set() - if ( - parent_handle is not None - and parent_handle.record()["status"] == "in_progress" - ): - _ = await parent_handle.result() - if parent_lane_pending: - await bus.chat_lane.mark_passive_done("proof", "parent") - await runtime.shutdown() - await manager.terminate_all() - await event_bus.aclose() - sessions.close() - await bus.aclose() - unregister_test_model_provider(workspace) - - -@pytest.mark.asyncio -async def test_recursive_candidate_trajectory_passes_real_production_oracle( - tmp_path: Path, -) -> None: - observation = await _run_trajectory(tmp_path) - - assert_recursive_candidate_trajectory(observation) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ( - "mutation", - "forged_domain", - "fake_tool_success", - "validation_runtime", - "error", - ), - [ - ("stable_misbinding", False, False, "stable", "没有绑定 latest"), - ("fake_tool_success", False, True, "latest", "真实 completed tool item"), - ("fake_domain_success", True, False, "latest", "领域状态"), - ], -) -async def test_recursive_candidate_trajectory_rejects_real_seam_mutants( - tmp_path: Path, - mutation: str, - forged_domain: bool, - fake_tool_success: bool, - validation_runtime: str, - error: str, -) -> None: - with pytest.raises(AssertionError, match=error): - _ = await _run_trajectory( - tmp_path / mutation, - forged_domain=forged_domain, - fake_tool_success=fake_tool_success, - validation_runtime=validation_runtime, - ) diff --git a/tests/test_agent_core_p2_reasoner.py b/tests/test_agent_core_p2_reasoner.py deleted file mode 100644 index e463c347e..000000000 --- a/tests/test_agent_core_p2_reasoner.py +++ /dev/null @@ -1,2826 +0,0 @@ -import asyncio -import json -import logging -from dataclasses import dataclass -from datetime import UTC, datetime -from types import MappingProxyType, SimpleNamespace -from typing import Any, cast -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from agent.control.turn_scope import ( - ToolGrant, - TurnExecutionScope, - bind_turn_scope, - reset_turn_scope, -) -from agent.core.passive_turn import ( - DefaultReasoner, - _PassThroughTurn, - _load_model_continuation, - _model_continuation_state, - _prompt_cache_key, -) -from agent.control.ports import TurnUserInput -from agent.core.runtime_support import SessionLike, ToolDiscoveryState -from agent.lifecycle.types import AfterStepCtx -from agent.looping.ports import LLMConfig -from plugins.compaction.engine import ( - CommittedContextUnit, - ContextCompactionError, - ContextPayloadSegments, - SUMMARY_HEADINGS, -) -from agent.plugin_composition import ( - LLMResponse, - ModelContinuation, - ModelRequest, - ModelRole, - ModelUsage, - ToolCall, -) -from agent.plugin_composition import ContextLengthError -from agent.tools.base import Tool -from agent.tools.registry import ToolRegistry -from agent.tools.tool_search import ToolSearchTool -from bus.event_bus import EventBus -from bus.events_lifecycle import ToolCallCompleted, ToolCallStarted, TurnOutputCompleted -from core.error_context import ( - current_provider_attempt, - current_provider_call_id, - current_provider_operation, -) -from plugins.compaction.runtime import CompactionProjection -from session.manager import Session -from session.store import CompactionHead -from tests.model_plugin_fakes import BoundChatModelFake -from tests.compaction_fakes import install_test_projection -from plugins.compaction.plugin import _CompactionTurn - - -@dataclass -class LLMServices: - """Legacy-shaped test input used only to build bound model fakes.""" - - provider: Any - light_provider: Any - fallback_provider: Any | None = None - fallback_model: str = "" - - -class _ProviderContextBudget: - context_window = 1_000_000 - - def estimate_context_tokens( - self, - messages: list[dict], - tools: list[dict], - ) -> int: - return max( - 1, - len(json.dumps([messages, tools], ensure_ascii=False)) // 3, - ) - - def estimate_appended_message_tokens(self, messages: list[dict]) -> int: - if not messages: - return 0 - return max(1, len(json.dumps(messages, ensure_ascii=False)) // 3) - - -class _DummyTool(Tool): - def __init__(self, name: str = "dummy") -> None: - self._name = name - self.calls: list[dict[str, Any]] = [] - - @property - def name(self) -> str: - return self._name - - @property - def description(self) -> str: - return self._name - - @property - def parameters(self) -> dict: - properties: dict[str, Any] = {"x": {"type": "integer"}} - if self._name == "message_push": - properties["message"] = {"type": "string"} - return {"type": "object", "properties": properties, "required": []} - - async def execute(self, **kwargs: Any) -> str: - self.calls.append(kwargs) - return f"{self._name}-ok" - - -class _InflateTool(Tool): - name = "inflate_probe" - description = "inflate_probe" - parameters = {"type": "object", "properties": {}, "required": []} - - async def execute(self, **kwargs: Any) -> str: - return f"payload-{kwargs.get('value', '')}-" + ("x" * 2400) - - -class _Provider(_ProviderContextBudget): - def __init__(self, responses: list[LLMResponse]) -> None: - self._responses = list(responses) - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - if not self._responses: - raise AssertionError("provider.chat called more than expected") - return self._responses.pop(0) - - -class _TimeoutProvider(_ProviderContextBudget): - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - raise asyncio.TimeoutError - - -class _UnknownWindowOverflowProvider(_ProviderContextBudget): - context_window = 0 - - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - raise ContextLengthError("provider context overflow") - - -class _MandatoryCompactionRuntime: - """Provide the narrow projection port required by reasoner test turns.""" - - @staticmethod - def _history(session: SessionLike) -> list[dict[str, Any]]: - return [dict(message) for message in session.get_history(max_messages=500)] - - async def projection( - self, - session: SessionLike, - *, - prefix: list[dict[str, Any]], - current_anchor: list[dict[str, Any]], - pending: list[dict[str, Any]], - ) -> CompactionProjection: - history = self._history(session) - units = tuple( - CommittedContextUnit( - source_from_seq=index, - consolidated_through_seq=index, - source_message_ids=(f"test-message-{index}",), - messages=(dict(message),), - message_refs=((f"test-message-{index}", index),), - ) - for index, message in enumerate(history) - ) - return CompactionProjection( - segments=ContextPayloadSegments( - prefix=tuple(prefix), - committed_units=units, - current_anchor=tuple(current_anchor), - pending=tuple(pending), - ), - active=None, - head=CompactionHead( - session_key=str(getattr(session, "key", "test-session")), - parent_generation=0, - next_generation=1, - ), - ) - - async def recover_pending(self, session: object) -> None: - return None - - async def commit_checkpoint(self, *args: Any, **kwargs: Any) -> Any: - raise AssertionError("test compaction gate unexpectedly attempted a commit") - - def checkpoint_suppresses_post_commit( - self, - _session_key: str, - _checkpoint: object, - ) -> bool: - return False - - -class _CommittableCompactionRuntime(_MandatoryCompactionRuntime): - """Commit 直接成功,供真实压缩路径(overflow 强制压缩 / 初始压缩)测试使用。""" - - def __init__(self) -> None: - self.commit_count = 0 - - async def commit_checkpoint( - self, - session: SessionLike, - checkpoint: Any, - *, - head: Any, - scope_channel: str = "", - scope_chat_id: str = "", - ) -> SimpleNamespace: - self.commit_count += 1 - return SimpleNamespace(generation=checkpoint.generation) - - -def _build_reasoner(**kwargs: Any) -> DefaultReasoner: - """Construct a reasoner with the mandatory session compaction runtime.""" - - llm = kwargs.pop("llm") - agent_model = BoundChatModelFake(llm.provider, model="m") - fallback_provider = llm.fallback_provider or llm.provider - fallback_model = BoundChatModelFake( - fallback_provider, - model=llm.fallback_model or "m", - role=ModelRole.DEFAULT, - ) - runtime = kwargs.pop("compaction_runtime", None) - reasoner = DefaultReasoner(**kwargs) - reasoner._test_compaction_runtime = runtime - reasoner._test_agent_model = agent_model - reasoner._test_fallback_model = fallback_model - return reasoner - - -def test_continuation_state_is_exact_binding_scoped() -> None: - provider = _Provider([]) - model = BoundChatModelFake(provider) - continuation = ModelContinuation( - binding_id=model.descriptor.binding_id, - payload={"response_id": "opaque"}, - ) - - state = _model_continuation_state(continuation) - assert state == { - "schema_version": 2, - "binding_id": model.descriptor.binding_id, - "payload": {"response_id": "opaque"}, - } - loaded = _load_model_continuation( - [{"role": "assistant", "model_state": state}], model - ) - assert loaded is not None - assert loaded.payload["response_id"] == "opaque" - other = BoundChatModelFake(_Provider([])) - assert ( - _load_model_continuation([{"role": "assistant", "model_state": state}], other) - is None - ) - assert ( - _load_model_continuation( - [ - {"role": "assistant", "model_state": state}, - {"role": "assistant", "content": "newer response without state"}, - {"role": "user", "content": "next turn"}, - ], - model, - ) - is None - ) - assert ( - _load_model_continuation( - [ - { - "role": "assistant", - "model_state": { - "schema_version": 1, - "runtime_id": model.descriptor.model_id, - "transport": "responses", - "model": model.descriptor.model, - "items": [], - }, - } - ], - model, - ) - is None - ) - - -def test_compaction_clears_continuation_and_anonymizes_cache_key() -> None: - provider = _Provider([LLMResponse(content="ok")]) - model = BoundChatModelFake(provider) - continuation = ModelContinuation( - binding_id=model.descriptor.binding_id, - payload={"response_id": "must-not-cross-compaction"}, - ) - prepared = SimpleNamespace(changed=True, auxiliary_usages=()) - gate = SimpleNamespace( - pending_start=0, - prepare=AsyncMock(return_value=prepared), - record_response=AsyncMock(), - ) - state = SimpleNamespace( - provider_call_ordinal=0, - gate=gate, - agent_model=model, - continuation=continuation, - first_any_logged=False, - first_thinking_logged=False, - first_answer_logged=False, - call_started_at=0.0, - ) - reasoner = _build_reasoner( - llm=LLMServices(provider=provider, light_provider=provider), - llm_config=LLMConfig(max_tokens=128), - tools=ToolRegistry(), - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - asyncio.run( - reasoner._call_provider( - cast(Any, state), - [{"role": "user", "content": "hello"}], - tools=[], - max_tokens=128, - cache_namespace="private-session-key", - ) - ) - - assert "model_state" not in provider.calls[0] - cache_key = provider.calls[0]["cache_namespace"] - assert cache_key == _prompt_cache_key(model, "private-session-key") - assert cache_key != "private-session-key" - assert "private-session-key" not in str(cache_key) - - -def test_public_mapping_tool_arguments_execute_as_runtime_owned_dict() -> None: - provider = _Provider([]) - tool = _DummyTool() - tools = ToolRegistry() - tools.register(tool) - reasoner = _build_reasoner( - llm=LLMServices(provider=provider, light_provider=provider), - llm_config=LLMConfig(max_iterations=4, max_tokens=128), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - class _PublicModel(BoundChatModelFake): - def __init__(self) -> None: - super().__init__(provider, model="m") - self.responses = [ - LLMResponse( - content="", - tool_calls=[ - ToolCall( - "call-1", - "dummy", - MappingProxyType({"x": 7}), - ) - ], - ), - LLMResponse(content="done"), - ] - - async def complete(self, _request: ModelRequest) -> LLMResponse: - return self.responses.pop(0) - - model = _PublicModel() - reasoner._test_agent_model = model - reasoner._test_fallback_model = model - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "call the tool"}], - ) - ) - - assert result.reply == "done" - assert tool.calls == [{"x": 7}] - assert type(result.tool_chain[0]["calls"][0]["arguments"]) is dict - - -@pytest.mark.parametrize( - "invalid_kind", - ("non_string_key", "non_finite", "object", "cycle"), -) -def test_invalid_public_tool_arguments_fail_before_execution( - invalid_kind: str, -) -> None: - if invalid_kind == "non_string_key": - arguments: Any = MappingProxyType({1: "x"}) - elif invalid_kind == "non_finite": - arguments = {"x": float("nan")} - elif invalid_kind == "object": - arguments = {"x": object()} - else: - arguments = {} - arguments["self"] = arguments - - provider = _Provider([]) - tool = _DummyTool() - tools = ToolRegistry() - tools.register(tool) - reasoner = _build_reasoner( - llm=LLMServices(provider=provider, light_provider=provider), - llm_config=LLMConfig(max_iterations=4, max_tokens=128), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - class _InvalidModel(BoundChatModelFake): - async def complete(self, _request: ModelRequest) -> LLMResponse: - return LLMResponse( - content="", - tool_calls=[ToolCall("call-1", "dummy", arguments)], - ) - - model = _InvalidModel(provider, model="m") - reasoner._test_agent_model = model - reasoner._test_fallback_model = model - - with pytest.raises(ValueError, match="model JSON"): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "call the tool"}], - ) - ) - assert tool.calls == [] - - -async def _run_with_compaction_gate( - reasoner: DefaultReasoner, - initial_messages: list[dict[str, Any]], - **kwargs: Any, -): - """Run the legacy-shaped fixture through the required compaction gate.""" - - payload = [dict(message) for message in initial_messages] - if not payload or payload[0].get("role") != "system": - payload.insert(0, {"role": "system", "content": "test context"}) - history = [dict(message) for message in payload[1:]] - session = Session( - key="test:reasoner", - created_at=datetime(2026, 8, 8, tzinfo=UTC), - messages=history, - last_consolidated=0, - ) - runtime = reasoner._test_compaction_runtime - if runtime is None: - projection = _PassThroughTurn(history) - else: - async def observe(_key: object, _payload: object) -> None: - return None - - await runtime.recover_pending(session) - runtime_projection = await runtime.projection( - session, - prefix=[], - current_anchor=[], - pending=[], - ) - projection = _CompactionTurn( - cast(Any, SimpleNamespace(observe=observe)), - runtime, - cast(Any, session), - runtime_projection, - keep_recent_tokens=1, - ) - state = reasoner._build_request_state( - projection=projection, - initial_messages=payload, - history_count=len(history), - attempt_replay=[], - prior_tool_groups=0, - channel="test", - chat_id="reasoner", - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - return await reasoner.run( - payload, - agent_model=reasoner._test_agent_model, - request_state=state, - **kwargs, - ) - - -def test_default_reasoner_runs_tool_loop_and_returns_reasoner_result(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "dummy", {})], - usage=ModelUsage(input_tokens=100, cached_input_tokens=40), - ), - LLMResponse( - content="final", - tool_calls=[], - usage=ModelUsage(input_tokens=120, cached_input_tokens=60), - ), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "final" - assert result.tools_used == ["dummy"] - assert result.tool_chain[0]["calls"][0]["name"] == "dummy" - assert result.visible_names is None - react_stats = result.react_stats - assert react_stats["iteration_count"] == 2 - assert react_stats["turn_input_sum_tokens"] >= react_stats["turn_input_peak_tokens"] - assert ( - react_stats["final_call_input_tokens"] == react_stats["turn_input_peak_tokens"] - ) - assert react_stats["cache_prompt_tokens"] == 220 - assert react_stats["cache_hit_tokens"] == 100 - first_messages = provider.calls[0]["messages"] - assert not any( - "未加载工具目录" in str(m.get("content", "")) for m in first_messages - ) - - -def test_default_reasoner_replays_interrupted_attempt_before_current_input(): - provider = _Provider([LLMResponse(content="final after u2", tool_calls=[])]) - timestamp = datetime.now(UTC) - inputs = ( - TurnUserInput("u1", 0, "first request", (), {}, timestamp), - TurnUserInput("u2", 1, "continue with node status", (), {}, timestamp), - ) - - class _Source: - async def lock(self) -> None: - return None - - def used_inputs(self) -> tuple[TurnUserInput, ...]: - return inputs - - replay = [ - {"role": "user", "content": "first request"}, - { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": "call-1", - "type": "function", - "function": {"name": "lookup", "arguments": "{}"}, - } - ], - }, - {"role": "tool", "tool_call_id": "call-1", "content": "node=ready"}, - {"role": "assistant", "content": "[execution attempt interrupted]"}, - ] - prior_tool_chain = [ - { - "text": "", - "calls": [ - { - "call_id": "call-1", - "name": "lookup", - "arguments": {}, - "result": "node=ready", - } - ], - } - ] - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=ToolRegistry(), - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - ), - ), - ) - session = SimpleNamespace( - key="mobile:one", - created_at=timestamp, - messages=[{"role": "user", "content": "old canonical"}], - get_history=lambda max_messages=40: [ - {"role": "user", "content": "old canonical"} - ], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="continue with node status", - media=[], - channel="mobile", - chat_id="one", - timestamp=timestamp, - metadata={ - "_control_turn_input_source": _Source(), - "_control_attempt_replay": replay, - "_control_prior_tool_chain": prior_tool_chain, - "_control_prior_input_count": 1, - }, - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert provider.calls[0]["messages"] == [ - {"role": "system", "content": "test context"}, - {"role": "user", "content": "old canonical"}, - *replay, - {"role": "user", "content": "continue with node status"}, - ] - assert result.reply == "final after u2" - assert result.tool_chain == prior_tool_chain - assert result.tools_used == ["lookup"] - assert "llm_user_content" not in result.context_retry - - -def test_default_reasoner_blocks_disabled_tool_even_if_model_calls_it(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "message_push", {"message": "天气"})], - ), - LLMResponse(content="最终天气", tool_calls=[]), - ] - ) - push = _DummyTool("message_push") - tools = ToolRegistry() - tools.register(push, always_on=True, risk="external-side-effect") - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "发天气"}], - disabled_tools={"message_push"}, - ) - ) - - first_tool_names = [ - schema["function"]["name"] for schema in provider.calls[0]["tools"] - ] - assert "message_push" not in first_tool_names - assert push.calls == [] - assert result.reply == "最终天气" - assert result.tools_used == [] - calls = result.tool_chain[0]["calls"] - assert calls[0]["name"] == "message_push" - assert calls[0]["status"] == "blocked" - - -def test_default_reasoner_does_not_interpret_legacy_memory_write_metadata(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "memorize", {"summary": "x"})], - ), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register( - _DummyTool("memorize"), - always_on=True, - risk="write", - source_type="builtin", - source_name="memory", - ) - tools.register( - _DummyTool("recall_memory"), - always_on=True, - risk="read-only", - source_type="builtin", - source_name="memory", - ) - tools.register(_DummyTool("read_file"), always_on=True, risk="read-only") - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - ), - ), - ) - session = SimpleNamespace( - key="telegram:123", - created_at=datetime(2026, 4, 5, 12, 0, 0, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=40: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="hi", - media=[], - channel="telegram", - chat_id="123", - timestamp=datetime(2026, 4, 5, 12, 0, 0), - metadata={"disable_memory_writes": True}, - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - first_tools = cast(list[dict[str, Any]], provider.calls[0]["tools"]) - first_tool_names = [schema["function"]["name"] for schema in first_tools] - assert "memorize" in first_tool_names - assert "recall_memory" in first_tool_names - assert "read_file" in first_tool_names - calls = cast(list[dict[str, Any]], result.tool_chain[0]["calls"]) - assert calls[0]["name"] == "memorize" - assert calls[0]["status"] != "blocked" - - -def test_default_reasoner_rejects_model_commit_role_override(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ - ToolCall( - "c1", - "message_push", - {"message": "hi", "_commit_role": "non_passive"}, - ) - ], - ), - LLMResponse(content="done", tool_calls=[]), - ] - ) - push = _DummyTool("message_push") - tools = ToolRegistry() - tools.register(push, always_on=True, risk="external-side-effect") - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "done" - assert push.calls == [] - - -def test_default_reasoner_injects_passive_commit_role_internally(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "message_push", {"message": "hi"})], - ), - LLMResponse(content="done", tool_calls=[]), - ] - ) - push = _DummyTool("message_push") - tools = ToolRegistry() - tools.register(push, always_on=True, risk="external-side-effect") - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "done" - assert push.calls == [{"message": "hi", "_commit_role": "passive"}] - - -def test_default_reasoner_tool_search_cannot_reunlock_disabled_tool(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ - ToolCall("s1", "tool_search", {"query": "select:message_push"}) - ], - ), - LLMResponse(content="最终天气", tool_calls=[]), - ] - ) - push = _DummyTool("message_push") - tools = ToolRegistry() - tools.register(ToolSearchTool(tools), always_on=True, risk="read-only") - tools.register(push, always_on=True, risk="external-side-effect") - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - ) - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "发天气"}], - disabled_tools={"message_push"}, - ) - ) - - first_tool_names = [ - schema["function"]["name"] for schema in provider.calls[0]["tools"] - ] - second_tool_names = [ - schema["function"]["name"] for schema in provider.calls[1]["tools"] - ] - assert "message_push" not in first_tool_names - assert "message_push" not in second_tool_names - assert push.calls == [] - assert result.reply == "最终天气" - assert result.visible_names is not None - assert "message_push" not in result.visible_names - - -def test_default_reasoner_zero_max_iterations_is_unlimited(): - provider = _Provider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "dummy", {})]), - LLMResponse(content="", tool_calls=[ToolCall("c2", "dummy", {})]), - LLMResponse(content="", tool_calls=[ToolCall("c3", "dummy", {})]), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tool = _DummyTool() - tools = ToolRegistry() - tools.register(tool, always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=0, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "final" - assert len(tool.calls) == 3 - - -def test_default_reasoner_context_pressure_policy_lives_in_after_step_plugin( - monkeypatch, -): - provider = _Provider( - [ - LLMResponse( - content="", tool_calls=[ToolCall("c1", "inflate_probe", {"value": 1})] - ), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register(_InflateTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=0, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "final" - assert len(provider.calls) == 2 - assert provider.calls[1]["tools"] - - -def test_default_reasoner_observes_tool_lifecycle_events(): - provider = _Provider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "dummy", {"x": 7})]), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tool = _DummyTool() - tools.register(tool, always_on=True) - event_bus = EventBus() - order: list[str] = [] - started_events: list[ToolCallStarted] = [] - completed_events: list[ToolCallCompleted] = [] - event_bus.on( - ToolCallStarted, - lambda event: order.append("started") or started_events.append(event), - ) - event_bus.on( - ToolCallCompleted, - lambda event: order.append("completed") or completed_events.append(event), - ) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - ), - ), - event_bus=event_bus, - ) - session = SimpleNamespace( - key="telegram:123", - created_at=datetime(2026, 4, 5, 12, 0, 0, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=40: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="hi", - media=[], - channel="telegram", - chat_id="123", - timestamp=datetime(2026, 4, 5, 12, 0, 0), - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert result.reply == "final" - assert order == ["started", "completed"] - assert started_events[0].session_key == "telegram:123" - assert started_events[0].channel == "telegram" - assert started_events[0].chat_id == "123" - assert started_events[0].iteration == 1 - assert started_events[0].call_id == "c1" - assert started_events[0].tool_name == "dummy" - assert started_events[0].arguments == {"x": 7} - assert completed_events[0].session_key == "telegram:123" - assert completed_events[0].call_id == "c1" - assert completed_events[0].tool_name == "dummy" - assert completed_events[0].arguments == {"x": 7} - assert completed_events[0].final_arguments == {"x": 7} - assert completed_events[0].status == "success" - assert completed_events[0].result_preview == "dummy-ok" - - -def test_default_reasoner_observes_output_completed_before_after_step(): - provider = _Provider([LLMResponse(content="final", tool_calls=[])]) - tools = ToolRegistry() - event_bus = EventBus() - order: list[str] = [] - completed_events: list[TurnOutputCompleted] = [] - event_bus.on( - TurnOutputCompleted, - lambda event: order.append("output_completed") - or completed_events.append(event), - ) - - async def slow_after_step(_event: AfterStepCtx) -> None: - order.append("after_step_start") - await asyncio.sleep(0.05) - order.append("after_step_end") - - event_bus.on(AfterStepCtx, slow_after_step) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - event_bus=event_bus, - ) - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - tool_event_session_key="telegram:123", - tool_event_channel="telegram", - tool_event_chat_id="123", - ) - ) - - assert result.reply == "final" - assert completed_events - assert completed_events[0].session_key == "telegram:123" - assert completed_events[0].channel == "telegram" - assert completed_events[0].chat_id == "123" - # 输出完成信号必须在 AfterStep 收尾完成之前发出,慢插件不得推迟解锁 - assert order.index("output_completed") < order.index("after_step_end") - - -def test_default_reasoner_observes_blocked_tool_lifecycle_events(): - provider = _Provider( - [ - LLMResponse( - content="", tool_calls=[ToolCall("c1", "hidden_tool", {"x": 1})] - ), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register(ToolSearchTool(tools), always_on=True, risk="read-only") - hidden = _DummyTool("hidden_tool") - tools.register(hidden) - event_bus = EventBus() - order: list[str] = [] - started_events: list[ToolCallStarted] = [] - completed_events: list[ToolCallCompleted] = [] - event_bus.on( - ToolCallStarted, - lambda event: order.append("started") or started_events.append(event), - ) - event_bus.on( - ToolCallCompleted, - lambda event: order.append("completed") or completed_events.append(event), - ) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - event_bus=event_bus, - ) - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - tool_event_session_key="telegram:123", - tool_event_channel="telegram", - tool_event_chat_id="123", - ) - ) - - assert result.reply == "final" - assert hidden.calls == [] - assert order == ["started", "completed"] - assert started_events[0].tool_name == "hidden_tool" - assert started_events[0].arguments == {"x": 1} - assert completed_events[0].tool_name == "hidden_tool" - assert completed_events[0].arguments == {"x": 1} - assert completed_events[0].final_arguments == {"x": 1} - assert completed_events[0].status == "blocked" - assert "select:hidden_tool" in completed_events[0].result_preview - - -def test_default_reasoner_unlocks_tool_search_visibility(): - provider = _Provider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("s1", "tool_search", {"query": "hidden"})], - ), - LLMResponse(content="", tool_calls=[ToolCall("h1", "hidden_tool", {})]), - LLMResponse(content="done", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register(ToolSearchTool(tools), always_on=True, risk="read-only") - hidden = _DummyTool("hidden_tool") - tools.register(hidden) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "done" - assert "hidden_tool" in result.tools_used - assert result.visible_names is not None - assert "hidden_tool" in result.visible_names - assert len(hidden.calls) == 1 - - -def test_default_reasoner_preflight_includes_deferred_tool_names(): - """调用方(如 _run_agent_loop)负责注入 deferred tools hint;run() 本身不再自动注入。""" - from agent.core.passive_turn import build_turn_injection_prompt - from agent.prompting import build_context_frame_content, build_context_frame_message - from agent.prompting import PromptSectionRender - - provider = _Provider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "dummy", {})]), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - tools.register( - _DummyTool("mcp_github__list_commits"), - source_type="mcp", - source_name="github", - ) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - ) - - # 调用方负责在调用 run() 前注入 hint。 - hint = build_turn_injection_prompt( - tools=tools, - tool_search_enabled=True, - visible_names=tools.get_always_on_names(), - ) - frame_content = build_context_frame_content( - [PromptSectionRender(name="tool_hint", content=hint, is_static=False)] - ) - initial_messages = [ - build_context_frame_message(frame_content), - {"role": "user", "content": "hi"}, - ] - asyncio.run(_run_with_compaction_gate(reasoner, initial_messages)) - - first_messages = provider.calls[0]["messages"] - preflight = next( - str(m.get("content", "")) - for m in first_messages - if "未加载工具目录" in str(m.get("content", "")) - ) - assert "未加载工具目录" in preflight - assert "mcp_github__list_commits" in preflight - assert "dummy" not in preflight - - -def test_default_reasoner_deferred_tool_direct_call_requires_select(): - provider = _Provider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "schedule", {})]), - LLMResponse(content="final", tool_calls=[]), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - tools.register(_DummyTool("schedule")) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert "schedule" not in result.tools_used - assert result.reply == "final" - tool_chain = list(result.tool_chain) - assert len(tool_chain) >= 1 - schedule_call = next( - (c for c in tool_chain[0]["calls"] if c["name"] == "schedule"), None - ) - assert schedule_call is not None - assert "select:" in schedule_call["result"] - assert "tool_search" in schedule_call["result"] - - -def test_default_reasoner_preloaded_tool_not_in_deferred_list(): - provider = _Provider([LLMResponse(content="done", tool_calls=[])]) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - tools.register(_DummyTool("schedule")) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - ) - - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - preloaded_tools={"schedule"}, - ) - ) - - first_messages = provider.calls[0]["messages"] - assert not any( - "未加载工具目录" in str(m.get("content", "")) for m in first_messages - ) - - -def test_default_reasoner_run_turn_uses_context_render(): - provider = _Provider([LLMResponse(content="done", tool_calls=[])]) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - build_messages=lambda **_: (_ for _ in ()).throw( - AssertionError("legacy build_messages should not be used") - ), - build_turn_injection_context=lambda **_: (_ for _ in ()).throw( - AssertionError("legacy turn_injection should not be used") - ), - ), - ), - ) - - session = SimpleNamespace( - key="cli:1", - created_at=datetime(2026, 4, 5, 12, 0, 0, tzinfo=UTC), - messages=[{"role": "assistant", "content": "old"}], - get_history=lambda max_messages=40: [{"role": "assistant", "content": "old"}], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="hi", - media=[], - channel="cli", - chat_id="1", - timestamp=datetime(2026, 4, 5, 12, 0, 0), - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert result.reply == "done" - - -def test_default_reasoner_session_history_read_false_reaches_provider_without_history(): - provider = _Provider([LLMResponse(content="done", tool_calls=[])]) - tools = ToolRegistry() - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=1, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "system"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ) - ), - ), - ) - session = SimpleNamespace( - key="programmatic:stateless", - created_at=datetime(2026, 8, 25, tzinfo=UTC), - get_history=lambda max_messages=500: [ - {"role": "assistant", "content": "must-not-reach-provider"} - ], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="current", - media=[], - channel="programmatic", - chat_id="stateless", - timestamp=datetime(2026, 8, 25, tzinfo=UTC), - metadata={"skip_session_history": True}, - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert result.reply == "done" - messages = provider.calls[0]["messages"] - assert [message["content"] for message in messages[:2]] == ["system", "current"] - assert all(message["content"] != "must-not-reach-provider" for message in messages) - - -@pytest.mark.asyncio -async def test_turn_scope_preloads_only_authorized_deferred_tool() -> None: - provider = _Provider([LLMResponse(content="done", tool_calls=[])]) - tools = ToolRegistry() - tools.register(ToolSearchTool(tools), always_on=True, risk="read-only") - tools.register(_DummyTool("scoped_decision")) - tools.register(_DummyTool("other_deferred")) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ) - ), - ), - ) - session = SimpleNamespace( - key="programmatic:scoped", - created_at=datetime(2026, 8, 25, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=500: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="decide", - media=[], - metadata={}, - channel="programmatic", - chat_id="scoped", - timestamp=datetime(2026, 8, 25, tzinfo=UTC), - ) - token = bind_turn_scope( - TurnExecutionScope( - preloaded_tools=("scoped_decision",), - tool_grant=ToolGrant.only(("scoped_decision",)), - ) - ) - try: - result = await reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - finally: - reset_turn_scope(token) - - assert result.reply == "done" - first_tool_names = { - schema["function"]["name"] for schema in provider.calls[0]["tools"] - } - assert first_tool_names == {"scoped_decision"} - - -@pytest.mark.asyncio -async def test_scoped_budget_adds_one_terminal_only_decision_round() -> None: - provider = _Provider( - [ - LLMResponse( - content="", - thinking="still investigating", - tool_calls=[], - continuation=ModelContinuation( - binding_id="fixture", - payload={"response_id": "before-terminal"}, - ), - ), - LLMResponse( - content="", - tool_calls=[ - ToolCall("hallucinated-research", "research", {}), - ToolCall("decision", "share_content", {}), - ToolCall("late-research", "research", {}), - ], - continuation=ModelContinuation( - binding_id="fixture", payload={"response_id": "terminal"} - ), - ), - ] - ) - tools = ToolRegistry() - research = _DummyTool("research") - decision = _DummyTool("share_content") - tools.register(research) - tools.register(decision) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=10, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ) - ), - ), - ) - session = SimpleNamespace( - key="programmatic:wake", - created_at=datetime(2026, 8, 25, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=500: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="investigate", - media=[], - metadata={}, - channel="wake", - chat_id="wake", - timestamp=datetime(2026, 8, 25, tzinfo=UTC), - ) - token = bind_turn_scope( - TurnExecutionScope( - preloaded_tools=("research", "share_content"), - terminal_tools=("share_content",), - tool_grant=ToolGrant.only(("research", "share_content")), - max_iterations=1, - ) - ) - try: - result = await reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - finally: - reset_turn_scope(token) - - assert result.tools_used == ["share_content"] - assert result.model_state is not None - assert result.model_state["payload"] == {"response_id": "terminal"} - assert research.calls == [] - assert len(provider.calls) == 2 - assert {schema["function"]["name"] for schema in provider.calls[1]["tools"]} == { - "share_content" - } - assert any( - message.get("role") == "user" - and "调查预算已经用完" in message.get("content", "") - for message in provider.calls[1]["messages"] - ) - - -def test_max_iteration_summary_persists_summary_continuation() -> None: - provider = _Provider( - [ - LLMResponse( - content="working", - tool_calls=[ToolCall("call-1", "dummy", {"x": 1})], - continuation=ModelContinuation( - binding_id="fixture", payload={"response_id": "tool"} - ), - ), - LLMResponse( - content="stopped cleanly", - continuation=ModelContinuation( - binding_id="fixture", payload={"response_id": "summary"} - ), - ), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool()) - reasoner = _build_reasoner( - llm=LLMServices(provider=provider, light_provider=provider), - llm_config=LLMConfig(max_iterations=1, max_tokens=128), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "work"}], - ) - ) - - assert result.reply == "stopped cleanly" - assert result.model_state is not None - assert result.model_state["payload"] == {"response_id": "summary"} - - -@pytest.mark.asyncio -async def test_scoped_terminal_correction_cannot_execute_non_terminal_tool() -> None: - provider = _Provider( - [ - LLMResponse(content="I will explain instead.", tool_calls=[]), - LLMResponse( - content="", - tool_calls=[ - ToolCall("malicious-research", "research", {}), - ToolCall("decision", "share_content", {}), - ], - ), - ] - ) - tools = ToolRegistry() - research = _DummyTool("research") - decision = _DummyTool("share_content") - tools.register(research) - tools.register(decision) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=3, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ) - ), - ), - ) - session = SimpleNamespace( - key="programmatic:wake", - created_at=datetime(2026, 8, 25, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=500: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="decide", - media=[], - metadata={}, - channel="wake", - chat_id="wake", - timestamp=datetime(2026, 8, 25, tzinfo=UTC), - ) - token = bind_turn_scope( - TurnExecutionScope( - preloaded_tools=("research", "share_content"), - terminal_tools=("share_content",), - tool_grant=ToolGrant.only(("research", "share_content")), - max_iterations=3, - ) - ) - try: - result = await reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - finally: - reset_turn_scope(token) - - assert result.tools_used == ["share_content"] - assert research.calls == [] - assert len(provider.calls) == 2 - assert {schema["function"]["name"] for schema in provider.calls[1]["tools"]} == { - "share_content" - } - - -@pytest.mark.asyncio -async def test_turn_scope_missing_preload_fails_before_provider_call() -> None: - provider = _Provider([LLMResponse(content="must not run", tool_calls=[])]) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=ToolRegistry(), - discovery=ToolDiscoveryState(), - tool_search_enabled=True, - context=cast(Any, SimpleNamespace(render=lambda *_args, **_kwargs: None)), - ) - session = SimpleNamespace( - key="programmatic:missing", - created_at=datetime(2026, 8, 25, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=500: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="decide", - media=[], - metadata={}, - channel="programmatic", - chat_id="missing", - timestamp=datetime(2026, 8, 25, tzinfo=UTC), - ) - token = bind_turn_scope( - TurnExecutionScope( - preloaded_tools=("missing_decision",), - tool_grant=ToolGrant.only(("missing_decision",)), - ) - ) - try: - with pytest.raises(RuntimeError, match="preload Tool 未注册: missing_decision"): - await reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - finally: - reset_turn_scope(token) - - assert provider.calls == [] - - -def test_default_reasoner_run_turn_reports_llm_timeout(): - provider = _TimeoutProvider() - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - ), - ), - ) - session = SimpleNamespace( - key="cli:1", - created_at=datetime(2026, 4, 5, 12, 0, 0, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=40: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="hi", - media=[], - channel="cli", - chat_id="1", - timestamp=datetime(2026, 4, 5, 12, 0, 0), - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert result.reply == "模型流响应中断,请刷新对话重试。" - assert len(provider.calls) == 1 - - -def test_default_reasoner_observes_output_completed_on_timeout_error(): - provider = _TimeoutProvider() - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - event_bus = EventBus() - completed_events: list[TurnOutputCompleted] = [] - event_bus.on(TurnOutputCompleted, completed_events.append) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - context=cast( - Any, - SimpleNamespace( - render=lambda request, **_: SimpleNamespace( - messages=[ - {"role": "system", "content": "test context"}, - *request.history, - {"role": "user", "content": request.current_message}, - ], - ), - ), - ), - event_bus=event_bus, - ) - session = SimpleNamespace( - key="cli:1", - created_at=datetime(2026, 4, 5, 12, 0, 0, tzinfo=UTC), - messages=[], - get_history=lambda max_messages=40: [], - last_consolidated=0, - ) - msg = SimpleNamespace( - content="hi", - media=[], - channel="cli", - chat_id="1", - timestamp=datetime(2026, 4, 5, 12, 0, 0), - ) - - result = asyncio.run( - reasoner.run_turn( - msg=msg, - session=cast(Any, session), - agent_model=reasoner._test_agent_model, - fallback_model=reasoner._test_fallback_model, - ) - ) - - assert result.reply == "模型流响应中断,请刷新对话重试。" - assert completed_events - assert completed_events[0].session_key == "cli:1" - assert completed_events[0].channel == "cli" - assert completed_events[0].chat_id == "1" - - -def test_empty_content_with_thinking_triggers_retry_and_succeeds(): - provider = _Provider( - [ - LLMResponse( - content=None, - tool_calls=[], - thinking="长思考过程", - finish_reason="length", - ), - LLMResponse( - content="正式回复", - tool_calls=[], - thinking="新思考", - finish_reason="stop", - ), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "正式回复" - assert result.thinking == "新思考" - assert result.react_stats["finish_reasons"] == [ - "length", - "stop", - ] - retry_call = provider.calls[1] - assert retry_call["disable_thinking"] is True - assert [schema["function"]["name"] for schema in retry_call["tools"]] == ["dummy"] - assert len(provider.calls) == 2 - - -def test_empty_content_with_thinking_retry_can_enter_tool_loop(): - provider = _Provider( - [ - LLMResponse(content=None, tool_calls=[], thinking="需要写文件"), - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "dummy", {})], - thinking="调用工具", - ), - LLMResponse(content="已完成", tool_calls=[]), - ] - ) - tool = _DummyTool() - tools = ToolRegistry() - tools.register(tool, always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), - light_provider=cast(Any, provider), - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "已完成" - assert tool.calls == [{}] - assert len(provider.calls) == 3 - assert provider.calls[1]["disable_thinking"] is True - assert [schema["function"]["name"] for schema in provider.calls[1]["tools"]] == [ - "dummy" - ] - - -def test_empty_content_with_thinking_retry_still_empty_falls_back(): - provider = _Provider( - [ - LLMResponse(content=None, tool_calls=[], thinking="只有思考"), - LLMResponse(content=None, tool_calls=[], thinking=None), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "模型未返回可用回复,请重试。" - assert result.thinking == "只有思考" - assert len(provider.calls) == 2 - - -def test_empty_content_without_thinking_no_retry(): - provider = _Provider( - [ - LLMResponse(content=None, tool_calls=[], thinking=None), - ] - ) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - result = asyncio.run( - _run_with_compaction_gate(reasoner, [{"role": "user", "content": "hi"}]) - ) - - assert result.reply == "模型未返回可用回复,请重试。" - assert len(provider.calls) == 1 - - -def test_default_reasoner_uses_one_default_step_phase_pair(): - provider = _Provider([LLMResponse(content="done", tool_calls=[])]) - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - first = reasoner._runtime_step_phases() - second = reasoner._runtime_step_phases() - - assert second == first - assert second[0] is first[0] - assert second[1] is first[1] - - -# ── 首 token 观测:turn-first 里程碑 ───────────────────────────────── - - -def _milestone_events( - caplog: pytest.LogCaptureFixture, - event: str, -) -> list[dict[str, object]]: - return [ - cast(dict[str, object], record.akashic_fields) - for record in caplog.records - if getattr(record, "akashic_fields", None) is not None - and record.akashic_fields.get("event") == event - ] - - -def _counts_map(counts: str) -> dict[str, str]: - return dict(part.split("=", 1) for part in counts.split() if "=" in part) - - -def _provider_call_id(fields: dict[str, object]) -> str: - return _counts_map(cast(str, fields["counts"]))["provider_call_id"] - - -class _FakeClock: - """Controllable monotonic clock;只由测试显式推进,不 sleep。""" - - def __init__(self) -> None: - self.now = 1_000.0 - - def __call__(self) -> float: - return self.now - - def advance_ms(self, ms: float) -> None: - self.now += ms / 1000.0 - - -class _BlockedRequestStartProvider(_ProviderContextBudget): - """chat 返回前人为阻塞 100ms(模拟请求建立/上游等待),再回传首 delta。""" - - def __init__(self, response: LLMResponse, clock: _FakeClock) -> None: - self._response = response - self._clock = clock - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - self._clock.advance_ms(100.0) - delta_sink = kwargs.get("on_content_delta") - if delta_sink is not None: - await delta_sink({"thinking_delta": "deliberate"}) - return self._response - - -class _DeltaEmitterProvider(_ProviderContextBudget): - """每次 chat 都流式回传 thinking+content delta,模拟真实流式消费。""" - - def __init__(self, responses: list[LLMResponse]) -> None: - self._responses = list(responses) - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - delta_sink = kwargs.get("on_content_delta") - if delta_sink is not None: - await delta_sink({"thinking_delta": "ponder"}) - await delta_sink({"content_delta": "draft"}) - if not self._responses: - raise AssertionError("provider.chat called more than expected") - return self._responses.pop(0) - - -class _ToolFirstProvider(_ProviderContextBudget): - """纯 tool-call 响应:不流式任何 delta,first_any 只能来自 tool kind。""" - - def __init__( - self, responses: list[LLMResponse], clock: _FakeClock | None = None - ) -> None: - self._responses = list(responses) - self._clock = clock - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - if self._clock is not None: - self._clock.advance_ms(100.0) - if not self._responses: - raise AssertionError("provider.chat called more than expected") - return self._responses.pop(0) - - -class _FailingProvider(_ProviderContextBudget): - """普通 provider 异常:attempt 必须以 error 终态闭合。""" - - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - raise RuntimeError("provider exploded") - - -class _CancelledProvider(_ProviderContextBudget): - """provider 抛 CancelledError:attempt 必须以 cancelled 终态闭合。""" - - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - raise asyncio.CancelledError - - -class _OverflowThenSuccessProvider(_ProviderContextBudget): - """attempt1 抛 ContextLengthError;强制压缩 summary 返回合法摘要;attempt2 成功。""" - - def __init__(self, response: LLMResponse) -> None: - self._response = response - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - if kwargs.get("on_content_delta") is None: - return LLMResponse(content="\n".join(SUMMARY_HEADINGS)) - business_calls = [ - call for call in self.calls if call.get("on_content_delta") is not None - ] - if len(business_calls) == 1: - raise ContextLengthError("provider context overflow") - return self._response - - -class _SlowCompactionProvider(_ProviderContextBudget): - """初始压缩 summary 慢(500ms),业务 chat TTFT 快(100ms),验证两者分离。""" - - context_window = 1_000_000 - - def __init__(self, response: LLMResponse, clock: _FakeClock) -> None: - self._response = response - self._clock = clock - self.calls: list[dict[str, Any]] = [] - - def estimate_context_tokens( - self, - messages: list[dict], - tools: list[dict], - ) -> int: - if any( - "" in str(message.get("content", "")) - for message in messages - ): - return 10 - return 900_000 - - def estimate_appended_message_tokens(self, messages: list[dict]) -> int: - return 3 - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - if kwargs.get("on_content_delta") is None: - self._clock.advance_ms(500.0) - return LLMResponse(content="\n".join(SUMMARY_HEADINGS)) - self._clock.advance_ms(100.0) - delta_sink = kwargs.get("on_content_delta") - if delta_sink is not None: - await delta_sink({"thinking_delta": "deliberate"}) - return self._response - - -class _BoundaryHitProvider(_ProviderContextBudget): - """估算越过软边界:无压缩候选时 gate 报 error;summary 阶段抛 CancelledError 时 gate 报 cancelled。""" - - context_window = 1_000_000 - - def __init__(self) -> None: - self.calls: list[dict[str, Any]] = [] - - def estimate_context_tokens( - self, - messages: list[dict], - tools: list[dict], - ) -> int: - if any( - "" in str(message.get("content", "")) - for message in messages - ): - return 10 - return 900_000 - - def estimate_appended_message_tokens(self, messages: list[dict]) -> int: - return 3 - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - raise asyncio.CancelledError - - -class _SlowSinkProvider(_ProviderContextBudget): - """下游回调消费慢(200ms):first-delta 采样必须发生在回调之前,不被污染。""" - - def __init__(self, response: LLMResponse, clock: _FakeClock) -> None: - self._response = response - self._clock = clock - self.calls: list[dict[str, Any]] = [] - - async def chat(self, **kwargs: Any) -> LLMResponse: - self.calls.append(kwargs) - delta_sink = kwargs.get("on_content_delta") - if delta_sink is not None: - await delta_sink({"thinking_delta": "fast"}) - self._clock.advance_ms(200.0) - return self._response - - -def _compaction_reasoner( - provider: object, - runtime: _CommittableCompactionRuntime, -) -> DefaultReasoner: - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - return _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - compaction_runtime=runtime, - ) - - -def _single_tool_round_reasoner(provider: object) -> DefaultReasoner: - tools = ToolRegistry() - tools.register(_DummyTool(), always_on=True) - return _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=tools, - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - -async def _stream_delta_sink(delta: dict[str, str]) -> None: - return None - - -def test_turn_first_ttft_includes_request_establishment_delay( - monkeypatch, - caplog: pytest.LogCaptureFixture, -) -> None: - clock = _FakeClock() - monkeypatch.setattr("time.monotonic", clock) - provider = _BlockedRequestStartProvider(LLMResponse(content="final"), clock) - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "final" - starts = _milestone_events(caplog, "tl:provider.call.start") - assert len(starts) == 1 - call_id = _provider_call_id(starts[0]) - assert len(call_id) == 32 - assert str(starts[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - first_thinking = _milestone_events(caplog, "tl:turn.first_thinking") - assert len(first_thinking) == 1 - assert str(first_thinking[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - duration_ms = first_thinking[0].get("duration_ms") - assert isinstance(duration_ms, (int, float)) - assert duration_ms >= 100.0 - done = _milestone_events(caplog, "tl:provider.call.done") - assert len(done) == 1 - assert done[0].get("outcome") == "done" - assert str(done[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - - -def test_two_tool_rounds_emit_single_turn_first( - caplog: pytest.LogCaptureFixture, -) -> None: - provider = _DeltaEmitterProvider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "dummy", {})]), - LLMResponse(content="final", tool_calls=[]), - ] - ) - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "final" - assert result.tools_used == ["dummy"] - assert len(provider.calls) == 2 - starts = _milestone_events(caplog, "tl:provider.call.start") - assert len(starts) == 2 - first_call_id = _provider_call_id(starts[0]) - second_call_id = _provider_call_id(starts[1]) - assert first_call_id != second_call_id - assert [str(item.get("counts")) for item in starts] == [ - f"call_ordinal=1 provider_attempt=1 provider_call_id={first_call_id}", - f"call_ordinal=2 provider_attempt=1 provider_call_id={second_call_id}", - ] - done = _milestone_events(caplog, "tl:provider.call.done") - assert len(done) == 2 - assert [str(item.get("outcome")) for item in done] == ["done", "done"] - assert len(_milestone_events(caplog, "tl:turn.first_any")) == 1 - assert len(_milestone_events(caplog, "tl:turn.first_thinking")) == 1 - assert len(_milestone_events(caplog, "tl:turn.first_answer")) == 1 - # turn.first 携带发出该事件时所属逻辑 call 的 provider_call_id。 - assert ( - _provider_call_id(_milestone_events(caplog, "tl:turn.first_any")[0]) - == first_call_id - ) - assert ( - _provider_call_id(_milestone_events(caplog, "tl:turn.first_thinking")[0]) - == first_call_id - ) - # _DeltaEmitterProvider 每轮同时发 thinking+content:first_answer 也在 round1 发出。 - assert ( - _provider_call_id(_milestone_events(caplog, "tl:turn.first_answer")[0]) - == first_call_id - ) - - -def test_tool_call_first_records_turn_first_any( - monkeypatch, - caplog: pytest.LogCaptureFixture, -) -> None: - clock = _FakeClock() - monkeypatch.setattr("time.monotonic", clock) - provider = _ToolFirstProvider( - [ - LLMResponse(content="", tool_calls=[ToolCall("c1", "dummy", {})]), - LLMResponse(content="final", tool_calls=[]), - ], - clock=clock, - ) - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "final" - first_any = _milestone_events(caplog, "tl:turn.first_any") - assert len(first_any) == 1 - call_id = _provider_call_id(first_any[0]) - assert str(first_any[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id} kind=tool" - ) - first_any_duration = first_any[0].get("duration_ms") - assert isinstance(first_any_duration, (int, float)) - assert first_any_duration >= 100.0 - assert not _milestone_events(caplog, "tl:turn.first_thinking") - assert not _milestone_events(caplog, "tl:turn.first_answer") - - -# ── provider call / compaction 里程碑:结构化 outcome 与 attempt 闭合 ────────── - - -def test_initial_compaction_slow_keeps_provider_ttft_separate( - monkeypatch, - caplog: pytest.LogCaptureFixture, -) -> None: - clock = _FakeClock() - monkeypatch.setattr("time.monotonic", clock) - runtime = _CommittableCompactionRuntime() - provider = _SlowCompactionProvider(LLMResponse(content="final"), clock) - reasoner = _compaction_reasoner(provider, runtime) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [ - {"role": "user", "content": "old one"}, - {"role": "user", "content": "current"}, - ], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "final" - assert runtime.commit_count == 1 - prepares = _milestone_events(caplog, "tl:request_projection.prepare.done") - assert len(prepares) == 1 - assert prepares[0].get("outcome") == "done" - prepare_duration = prepares[0].get("duration_ms") - assert isinstance(prepare_duration, (int, float)) - assert prepare_duration >= 500.0 - starts = _milestone_events(caplog, "tl:provider.call.start") - assert len(starts) == 1 - call_id = _provider_call_id(starts[0]) - assert str(prepares[0].get("counts")) == ( - f"call_ordinal=1 provider_call_id={call_id} " - "trigger=soft_limit force=false compacted=true" - ) - assert str(starts[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - # 初始 compaction gate 与业务 call 属于同一逻辑调用:call_id 一致。 - assert ( - _provider_call_id(_milestone_events(caplog, "tl:request_projection.prepare.start")[0]) - == call_id - ) - first_thinking = _milestone_events(caplog, "tl:turn.first_thinking") - assert len(first_thinking) == 1 - assert str(first_thinking[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - first_duration = first_thinking[0].get("duration_ms") - assert isinstance(first_duration, (int, float)) - assert first_duration < 200.0 - assert first_duration < prepare_duration - - -def test_context_overflow_sequence_closes_retry_then_attempt_two( - caplog: pytest.LogCaptureFixture, -) -> None: - runtime = _CommittableCompactionRuntime() - provider = _OverflowThenSuccessProvider(LLMResponse(content="recovered")) - reasoner = _compaction_reasoner(provider, runtime) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [ - {"role": "user", "content": "old one"}, - {"role": "user", "content": "current"}, - ], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "recovered" - assert runtime.commit_count == 1 - starts = _milestone_events(caplog, "tl:provider.call.start") - assert len(starts) == 2 - attempt_one_id = _provider_call_id(starts[0]) - attempt_two_id = _provider_call_id(starts[1]) - # 两个 overflow attempts 属于同一个逻辑 call:共享 provider_call_id。 - assert attempt_one_id == attempt_two_id - assert [str(item.get("counts")) for item in starts] == [ - f"call_ordinal=1 provider_attempt=1 provider_call_id={attempt_one_id}", - f"call_ordinal=1 provider_attempt=2 provider_call_id={attempt_one_id}", - ] - retry = _milestone_events(caplog, "tl:provider.call.retry") - assert len(retry) == 1 - assert retry[0].get("outcome") == "context_overflow" - assert retry[0].get("duration_ms") is not None - assert str(retry[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={attempt_one_id}" - ) - prepares = _milestone_events(caplog, "tl:request_projection.prepare.done") - assert [str(item.get("counts")) for item in prepares] == [ - f"call_ordinal=1 provider_call_id={attempt_one_id} " - "trigger=soft_limit force=false compacted=false", - f"call_ordinal=1 provider_call_id={attempt_one_id} " - "trigger=context_overflow force=true compacted=true", - ] - assert all(item.get("outcome") == "done" for item in prepares) - done = _milestone_events(caplog, "tl:provider.call.done") - assert len(done) == 1 - assert done[0].get("outcome") == "done" - assert str(done[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=2 provider_call_id={attempt_one_id}" - ) - assert done[0].get("duration_ms") is not None - assert not _milestone_events(caplog, "tl:provider.call.error") - assert not _milestone_events(caplog, "tl:provider.call.cancelled") - - -def test_provider_error_closes_attempt_with_error_outcome( - caplog: pytest.LogCaptureFixture, -) -> None: - provider = _FailingProvider() - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - with pytest.raises(RuntimeError, match="provider exploded"): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "boom"}], - ) - ) - - errors = _milestone_events(caplog, "tl:provider.call.error") - assert len(errors) == 1 - assert errors[0].get("outcome") == "error" - assert errors[0].get("duration_ms") is not None - call_id = _provider_call_id(errors[0]) - assert str(errors[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - assert not _milestone_events(caplog, "tl:provider.call.done") - assert not _milestone_events(caplog, "tl:provider.call.retry") - assert not _milestone_events(caplog, "tl:provider.call.cancelled") - - -def test_provider_cancelled_closes_attempt_with_cancelled_outcome( - caplog: pytest.LogCaptureFixture, -) -> None: - provider = _CancelledProvider() - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - with pytest.raises(asyncio.CancelledError): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "boom"}], - ) - ) - - cancelled = _milestone_events(caplog, "tl:provider.call.cancelled") - assert len(cancelled) == 1 - assert cancelled[0].get("outcome") == "cancelled" - assert cancelled[0].get("duration_ms") is not None - call_id = _provider_call_id(cancelled[0]) - assert str(cancelled[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - assert not _milestone_events(caplog, "tl:provider.call.done") - assert not _milestone_events(caplog, "tl:provider.call.error") - assert not _milestone_events(caplog, "tl:provider.call.retry") - - -def test_unknown_window_overflow_closes_attempt_with_error_outcome( - caplog: pytest.LogCaptureFixture, -) -> None: - provider = _UnknownWindowOverflowProvider() - reasoner = _build_reasoner( - llm=cast( - Any, - LLMServices( - provider=cast(Any, provider), light_provider=cast(Any, provider) - ), - ), - llm_config=LLMConfig(max_iterations=4, max_tokens=512), - tools=ToolRegistry(), - discovery=ToolDiscoveryState(), - tool_search_enabled=False, - ) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - with pytest.raises(ContextLengthError, match="provider context overflow"): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "overflow"}], - ) - ) - - assert len(provider.calls) == 1 - errors = _milestone_events(caplog, "tl:provider.call.error") - assert len(errors) == 1 - assert errors[0].get("outcome") == "error" - assert errors[0].get("duration_ms") is not None - call_id = _provider_call_id(errors[0]) - assert str(errors[0].get("counts")) == ( - f"call_ordinal=1 provider_attempt=1 provider_call_id={call_id}" - ) - assert not _milestone_events(caplog, "tl:provider.call.retry") - assert not _milestone_events(caplog, "tl:provider.call.done") - assert not _milestone_events(caplog, "tl:provider.call.cancelled") - - -def test_compaction_prepare_error_records_error_then_propagates( - caplog: pytest.LogCaptureFixture, -) -> None: - runtime = _CommittableCompactionRuntime() - provider = _BoundaryHitProvider() - reasoner = _compaction_reasoner(provider, runtime) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - with pytest.raises( - ContextCompactionError, - match="context_compaction_no_closed_prefix", - ): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "only one unit"}], - ) - ) - - prepare_errors = _milestone_events(caplog, "tl:request_projection.prepare.error") - assert len(prepare_errors) == 1 - assert prepare_errors[0].get("outcome") == "error" - assert prepare_errors[0].get("duration_ms") is not None - call_id = _provider_call_id(prepare_errors[0]) - assert str(prepare_errors[0].get("counts")) == ( - f"call_ordinal=1 provider_call_id={call_id} " "trigger=soft_limit force=false" - ) - assert not _milestone_events(caplog, "tl:request_projection.prepare.done") - assert not _milestone_events(caplog, "tl:request_projection.prepare.cancelled") - assert not _milestone_events(caplog, "tl:provider.call.start") - - -def test_compaction_prepare_cancelled_records_cancelled_then_propagates( - caplog: pytest.LogCaptureFixture, -) -> None: - runtime = _CommittableCompactionRuntime() - provider = _BoundaryHitProvider() - reasoner = _compaction_reasoner(provider, runtime) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - with pytest.raises(asyncio.CancelledError): - asyncio.run( - _run_with_compaction_gate( - reasoner, - [ - {"role": "user", "content": "old one"}, - {"role": "user", "content": "current"}, - ], - ) - ) - - prepare_cancelled = _milestone_events(caplog, "tl:request_projection.prepare.cancelled") - assert len(prepare_cancelled) == 1 - assert prepare_cancelled[0].get("outcome") == "cancelled" - assert prepare_cancelled[0].get("duration_ms") is not None - call_id = _provider_call_id(prepare_cancelled[0]) - assert str(prepare_cancelled[0].get("counts")) == ( - f"call_ordinal=1 provider_call_id={call_id} " "trigger=soft_limit force=false" - ) - assert not _milestone_events(caplog, "tl:request_projection.prepare.done") - assert not _milestone_events(caplog, "tl:request_projection.prepare.error") - assert not _milestone_events(caplog, "tl:provider.call.start") - - -def test_slow_downstream_callback_does_not_pollute_first_delta( - monkeypatch, - caplog: pytest.LogCaptureFixture, -) -> None: - clock = _FakeClock() - monkeypatch.setattr("time.monotonic", clock) - provider = _SlowSinkProvider(LLMResponse(content="final"), clock) - reasoner = _single_tool_round_reasoner(provider) - - with caplog.at_level(logging.INFO, logger="agent.core.passive_turn"): - result = asyncio.run( - _run_with_compaction_gate( - reasoner, - [{"role": "user", "content": "hi"}], - on_content_delta=_stream_delta_sink, - ) - ) - - assert result.reply == "final" - first_thinking = _milestone_events(caplog, "tl:turn.first_thinking") - assert len(first_thinking) == 1 - first_duration = first_thinking[0].get("duration_ms") - assert isinstance(first_duration, (int, float)) - assert first_duration < 150.0 - done = _milestone_events(caplog, "tl:provider.call.done") - assert len(done) == 1 - done_duration = done[0].get("duration_ms") - assert isinstance(done_duration, (int, float)) - assert done_duration >= 200.0 diff --git a/tests/test_agent_core_p3_context_store.py b/tests/test_agent_core_p3_context_store.py deleted file mode 100644 index 8a5f16613..000000000 --- a/tests/test_agent_core_p3_context_store.py +++ /dev/null @@ -1,123 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import MagicMock - -import pytest - -from agent.core.passive_support import ( - build_post_reply_context_budget, - to_history_messages, -) -from agent.core.passive_turn import DefaultContextStore -from bus.events import InboundMessage - - -class _DummySession: - def __init__(self) -> None: - self.messages = [ - { - "role": "user", - "content": "hello", - "tools_used": ["read_file"], - "tool_chain": [ - { - "text": "tool run", - "calls": [ - { - "call_id": "call-1", - "name": "read_file", - "arguments": {"path": "/tmp/a.txt"}, - "result": "ok", - } - ], - } - ], - }, - {"role": "assistant", "content": "world"}, - ] - - def get_history(self, max_messages: int = 500) -> list[dict]: - return self.messages[-max_messages:] - - -@pytest.mark.asyncio -async def test_default_context_store_prepares_only_history_and_skill_mentions() -> None: - context = SimpleNamespace( - skills=SimpleNamespace( - list_skill_records=MagicMock( - return_value=[ - SimpleNamespace(name="refactor"), - SimpleNamespace(name="known"), - ] - ) - ) - ) - bundle = await DefaultContextStore(context=cast(Any, context)).prepare( - msg=InboundMessage( - channel="cli", - sender="hua", - chat_id="1", - content="请用 $refactor 再来一次 $known $refactor", - timestamp=datetime(2026, 4, 4, 20, 0, 0), - ), - session_key="cli:1", - session=cast(Any, _DummySession()), - ) - - assert bundle.skill_mentions == ["refactor", "known"] - assert bundle.history_messages[0].tool_chain[0].calls[0].name == "read_file" - - -@pytest.mark.asyncio -async def test_default_context_store_can_omit_session_history() -> None: - context = SimpleNamespace( - skills=SimpleNamespace(list_skill_records=MagicMock(return_value=[])) - ) - bundle = await DefaultContextStore(context=cast(Any, context)).prepare( - msg=InboundMessage( - channel="scheduler", - sender="scheduler", - chat_id="job-1", - content="查询北京天气", - metadata={"skip_session_history": True}, - ), - session_key="scheduler:job-1", - session=cast(Any, _DummySession()), - ) - - assert bundle.history_messages == [] - - -def test_build_post_reply_context_budget_combines_history_and_prompt() -> None: - context = SimpleNamespace( - last_debug_breakdown=[ - SimpleNamespace(est_tokens=100), - SimpleNamespace(est_tokens=250), - ] - ) - budget = build_post_reply_context_budget( - context=cast(Any, context), - history=[{"role": "user", "content": "你好"}], - ) - assert "history_window" not in budget - assert budget["history_messages"] == 1 - assert budget["history_chars"] > 0 - assert budget["history_tokens"] == max(1, budget["history_chars"] // 3) - assert budget["prompt_tokens"] == 350 - assert budget["next_turn_baseline_tokens"] == budget["history_tokens"] + 350 - - -def test_history_tool_arguments_do_not_fall_back_to_empty_dict() -> None: - messages = [ - { - "role": "assistant", - "content": "", - "tool_chain": [{"calls": [{"name": "read_file", "arguments": None}]}], - } - ] - - with pytest.raises(TypeError, match=r"group=0 call=0 type=NoneType"): - to_history_messages(messages) diff --git a/tests/test_agent_core_p4_prompt_block.py b/tests/test_agent_core_p4_prompt_block.py deleted file mode 100644 index 51ca050b3..000000000 --- a/tests/test_agent_core_p4_prompt_block.py +++ /dev/null @@ -1,150 +0,0 @@ -from __future__ import annotations -from typing import Any, cast - -from pathlib import Path -from types import SimpleNamespace - -from agent.core.prompt_block import ( - ActiveSkillsPromptBlock, - BehaviorRulesPromptBlock, - IdentityPromptBlock, - SessionContextPromptBlock, - SkillsCatalogPromptBlock, - SystemPromptBuilder, - TurnContext, - VedaPromptBlock, -) -from prompts.agent import ( - build_agent_static_identity_prompt, - build_current_session_prompt, -) - - -class _Skills: - def get_always_skills(self) -> list[str]: - return ["always"] - - def load_skills_for_context(self, names: list[str]) -> str: - return "\n".join(names) - - def build_skills_summary(self) -> str: - return "summary" - - -def test_system_prompt_builder_uses_prompt_blocks_and_static_cache(tmp_path: Path): - builder = SystemPromptBuilder( - [ - IdentityPromptBlock(render_fn=lambda **_: "identity"), - ] - ) - ctx = TurnContext( - workspace=tmp_path, - skills=cast(Any, _Skills()), - skill_names=[], - channel=None, - chat_id=None, - ) - - first = builder.build(ctx) - second = builder.build(ctx) - - assert [item.content for item in first] == ["identity"] - assert [item.name for item in first] == ["identity"] - assert second[0].cache_hit is True - - -def test_system_prompt_builder_respects_disabled_sections(tmp_path: Path): - builder = SystemPromptBuilder( - [ - IdentityPromptBlock(render_fn=lambda **_: "identity"), - ] - ) - ctx = TurnContext( - workspace=tmp_path, - skills=cast(Any, _Skills()), - skill_names=[], - channel=None, - chat_id=None, - ) - - built = builder.build(ctx, disabled_sections={"identity"}) - - assert built == [] - - -def test_static_identity_prompt_exposes_veda_edit_boundary(tmp_path: Path): - prompt = build_agent_static_identity_prompt(workspace=tmp_path) - - assert f"{tmp_path.resolve()}/memory/VEDA.md" in prompt - assert "只有用户明确要求修改人格或 Veda 时" in prompt - assert "用户的长期 AI 伙伴" not in prompt - - -def test_veda_prompt_block_reloads_after_each_turn_build(tmp_path: Path): - path = tmp_path / "memory/VEDA.md" - path.parent.mkdir(parents=True) - path.write_text("first veda", encoding="utf-8") - builder = SystemPromptBuilder([VedaPromptBlock()]) - ctx = TurnContext( - workspace=tmp_path, - skills=cast(Any, _Skills()), - skill_names=[], - channel=None, - chat_id=None, - ) - - first = builder.build(ctx) - path.write_text("second veda", encoding="utf-8") - second = builder.build(ctx) - - assert [item.content for item in first] == ["first veda"] - assert [item.content for item in second] == ["second veda"] - assert second[0].cache_hit is False - - -def test_current_session_prompt_distinguishes_web_and_android_surfaces(): - web = build_current_session_prompt(channel="web", chat_id="desktop-chat") - mobile = build_current_session_prompt(channel="mobile", chat_id="phone-chat") - - assert "Channel: web" in web - assert "Chat ID: desktop-chat" in web - assert "Client Surface: WebChat" in web - assert "Client Device Context: 电脑网页端" in web - assert "Channel: mobile" in mobile - assert "Chat ID: phone-chat" in mobile - assert "Client Surface: Akashic Android" in mobile - assert "Client Device Context: Android 手机端" in mobile - - -def test_current_session_prompt_does_not_guess_unknown_channel_surface(): - prompt = build_current_session_prompt( - channel="custom_web_bridge", - chat_id="raw-chat-id", - ) - - assert "Channel: custom_web_bridge" in prompt - assert "Chat ID: raw-chat-id" in prompt - assert "Client Surface: Unknown" in prompt - assert "Client Device Context: Unknown" in prompt - assert "Client Surface: WebChat" not in prompt - assert "Client Surface: Akashic Android" not in prompt - - -def test_prompt_block_priorities_leave_spacing_for_future_inserts(): - priorities = [ - (VedaPromptBlock.label, VedaPromptBlock.priority), - (IdentityPromptBlock.label, IdentityPromptBlock.priority), - (BehaviorRulesPromptBlock.label, BehaviorRulesPromptBlock.priority), - (SkillsCatalogPromptBlock.label, SkillsCatalogPromptBlock.priority), - (SessionContextPromptBlock.label, SessionContextPromptBlock.priority), - (ActiveSkillsPromptBlock.label, ActiveSkillsPromptBlock.priority), - ] - - assert priorities == [ - ("veda", 5), - ("identity", 10), - ("behavior_rules", 15), - ("skills_catalog", 20), - ("session_context", 40), - ("active_skills", 50), - ] diff --git a/tests/test_agent_core_p5_agent_core.py b/tests/test_agent_core_p5_agent_core.py deleted file mode 100644 index 2357ef9c0..000000000 --- a/tests/test_agent_core_p5_agent_core.py +++ /dev/null @@ -1,631 +0,0 @@ -from __future__ import annotations - -from datetime import UTC, datetime -from types import SimpleNamespace -from typing import TypedDict, cast -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from agent.context import ContextBuilder -from agent.core.passive_turn import ContextStore, Reasoner, _NoopOutboundPort -from agent.core.runtime_support import SessionLike, TurnRunResult -from agent.core.passive_support import predict_current_user_source_ref -from agent.core.passive_turn import PassiveTurnDeps, PassiveTurnPipeline -from agent.core.types import ContextBundle -from agent.plugin_composition.channels import ( - ChannelDeliveryReceipt, - DeliveryStatus as ChannelDeliveryStatus, -) -from plugins.compaction.engine import CommittedContextUnit -from agent.plugin_composition import ChatModels -from agent.looping.ports import SessionServices -from agent.tools.registry import ToolRegistry -from agent.turns.outbound import OutboundDispatch, OutboundPort -from bus.event_bus import EventBus -from bus.events import ( - InboundMessage, - OutboundMessage, - TurnDisposition, -) -from agent.lifecycle.types import BeforeReasoningCtx, BeforeTurnCtx -from session.manager import SessionManager -from tests.model_plugin_fakes import build_test_chat_models - - -class _ChatModelsRunArgs(TypedDict): - chat_models: ChatModels - - -def _chat_models() -> _ChatModelsRunArgs: - return {"chat_models": build_test_chat_models(object())} - - -class _DummySession: - def __init__(self, key: str) -> None: - self.key = key - self._created_at = datetime(2026, 1, 1, tzinfo=UTC) - self.messages: list[dict] = [] - self.metadata: dict[str, object] = {} - self.last_consolidated = 0 - - @property - def created_at(self) -> datetime: - return self._created_at - - def get_history(self, max_messages: int = 500) -> list[dict]: - return self.messages[-max_messages:] - - def add_message( - self, - role: str, - content: str, - media=None, - **kwargs: object, - ) -> dict[str, object]: - if media is not None: - kwargs["media"] = media - message = {"role": role, "content": content, **kwargs} - self.messages.append(message) - return message - - def history_units(self) -> tuple[CommittedContextUnit, ...]: - """Render the fake's persisted rows as complete immutable history units.""" - - units: list[CommittedContextUnit] = [] - for index, message in enumerate(self.messages): - message_id = message.get("id") - if not isinstance(message_id, str) or not message_id: - message_id = f"{self.key}:{index}" - raw_seq = message.get("seq") - seq = ( - raw_seq - if isinstance(raw_seq, int) and not isinstance(raw_seq, bool) - else index - ) - units.append( - CommittedContextUnit( - source_from_seq=seq, - consolidated_through_seq=seq, - source_message_ids=(message_id,), - messages=(dict(message),), - message_refs=((message_id, seq),), - ) - ) - return tuple(units) - - -class _DeliveringOutboundPort: - async def dispatch(self, _outbound: OutboundDispatch) -> ChannelDeliveryReceipt: - return ChannelDeliveryReceipt( - delivery_id="test-delivery", - status=ChannelDeliveryStatus.DELIVERED, - ) - - -@pytest.mark.asyncio -async def test_noop_outbound_port_fails_loud_without_committed_dispatcher() -> None: - with pytest.raises(RuntimeError, match="committed Channel outbound port 未绑定"): - await _NoopOutboundPort().dispatch( - OutboundDispatch(channel="mobile", chat_id="device", content="hello") - ) - - -@pytest.mark.asyncio -async def test_passive_turn_runs_prepare_prompt_run_commit_in_order(): - order: list[str] = [] - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock( - side_effect=lambda **kwargs: order.append("prepare") - or ContextBundle( - skill_mentions=["refactor"], - ) - ) - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - side_effect=lambda request: order.append("render") - or SimpleNamespace( - system_prompt="system prompt", - messages=[], - ) - ) - ) - tools = SimpleNamespace( - set_context=MagicMock(side_effect=lambda **kwargs: order.append("tool_context")) - ) - reasoner = SimpleNamespace( - run_turn=AsyncMock( - side_effect=lambda *args, **kwargs: order.append("run") - or TurnRunResult( - reply="final \n§cited:[mem_1]§", - tools_used=["shell"], - tool_chain=[{"text": "done", "calls": []}], - thinking="think", - context_retry={"selected_plan": "full"}, - mobile_attention="confirmation", - ) - ), - ) - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="telegram:123:0"), - append_messages=AsyncMock(), - ), - presence=None, - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - outbound_port=cast(OutboundPort, _DeliveringOutboundPort()), - ) - ) - msg = InboundMessage( - channel="telegram", - sender="hua", - chat_id="123", - content="你好", - timestamp=datetime(2026, 4, 4, 22, 0, 0), - ) - - out = await pipeline.run(msg, "telegram:123", **_chat_models()) - - assert out.content == "final \n§cited:[mem_1]§" - assert out.metadata["mobile_attention"] == "confirmation" - assert order == ["prepare", "tool_context", "render", "run"] - assert context_store.prepare.await_args.kwargs["session_key"] == "telegram:123" - render_request = context.render.call_args.args[0] - assert render_request.current_message == "" - assert render_request.skill_names == ["refactor"] - tools.set_context.assert_called_once_with( - channel="telegram", - chat_id="123", - session_key="telegram:123", - turn_id="", - current_user_source_ref="telegram:123:0", - current_timestamp="2026-04-04T22:00:00", - ) - assert reasoner.run_turn.await_args.kwargs["skill_names"] == ["refactor"] - # AfterReasoning persists user+assistant messages to session - assert len(session.messages) == 2 - assert session.messages[0]["role"] == "user" - assert session.messages[1]["role"] == "assistant" - assert session.messages[1]["content"] == "final \n§cited:[mem_1]§" - - -@pytest.mark.asyncio -async def test_passive_turn_coerces_empty_reply_before_commit(): - session = _DummySession("cli:1") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="cli:1:0"), - append_messages=AsyncMock(), - ), - presence=None, - ), - ), - context_store=cast(ContextStore, context_store), - context=cast( - ContextBuilder, - SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - return_value=SimpleNamespace( - system_prompt="prompt", messages=[] - ) - ), - ), - ), - tools=cast( - ToolRegistry, - SimpleNamespace(set_context=MagicMock()), - ), - reasoner=cast( - Reasoner, - SimpleNamespace( - run_turn=AsyncMock(return_value=TurnRunResult(reply=None)), - ), - ), - outbound_port=cast(OutboundPort, _DeliveringOutboundPort()), - ) - ) - msg = InboundMessage( - channel="cli", - sender="hua", - chat_id="1", - content="hi", - metadata={"mobile_attention": "confirmation"}, - ) - - out = await pipeline.run(msg, "cli:1", **_chat_models()) - - assert "no response to give" in out.content - assert "mobile_attention" not in out.metadata - - -@pytest.mark.asyncio -async def test_passive_turn_before_reasoning_can_patch_context(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock( - return_value=ContextBundle( - skill_mentions=["old"], - ) - ), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - return_value=SimpleNamespace(system_prompt="prompt", messages=[]) - ) - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace( - run_turn=AsyncMock(return_value=TurnRunResult(reply="ok")), - ) - event_bus = EventBus() - - event_bus.on( - BeforeReasoningCtx, - lambda ctx: BeforeReasoningCtx( - session_key=ctx.session_key, - channel=ctx.channel, - chat_id=ctx.chat_id, - content=ctx.content, - timestamp=ctx.timestamp, - skill_names=["new"], - ), - ) - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="telegram:123:0"), - append_messages=AsyncMock(), - ), - presence=None, - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - event_bus=event_bus, - outbound_port=cast(OutboundPort, _DeliveringOutboundPort()), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - await pipeline.run(msg, "telegram:123", **_chat_models()) - - render_request = context.render.call_args.args[0] - assert render_request.skill_names == ["new"] - assert reasoner.run_turn.await_args.kwargs["skill_names"] == ["new"] - - -def test_predict_current_user_source_ref_uses_session_identity_owner(): - session = _DummySession("telegram:123") - session_manager = SimpleNamespace( - peek_next_message_id=MagicMock(return_value="telegram:123:42") - ) - - value = predict_current_user_source_ref( - session_manager=cast(SessionManager, session_manager), - session=cast(SessionLike, session), - ) - - assert value == "telegram:123:42" - session_manager.peek_next_message_id.assert_called_once_with(session.key) - - -@pytest.mark.asyncio -async def test_before_turn_abort_skips_reasoner_and_commit_and_dispatches(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])), - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace(run_turn=AsyncMock()) - event_bus = EventBus() - dispatch_port = AsyncMock(return_value=True) - - async def abort_handler(ctx): - ctx.abort = True - ctx.abort_reply = "blocked by policy" - return ctx - - event_bus.on(BeforeTurnCtx, abort_handler) - - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock( - return_value="telegram:123:0" - ), - ) - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - event_bus=event_bus, - outbound_port=cast(OutboundPort, dispatch_port), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - out = await pipeline.run( - msg, - "telegram:123", - dispatch_outbound=True, - **_chat_models(), - ) - - assert out.content == "blocked by policy" - assert out.turn_disposition is TurnDisposition.SHORT_CIRCUITED - # 不经过 reasoner 和持久化 - reasoner.run_turn.assert_not_called() - # 通过 outbound_port 实际 dispatch - dispatch_port.dispatch.assert_awaited_once() - dispatched = dispatch_port.dispatch.await_args.args[0] - assert dispatched.content == "blocked by policy" - - -@pytest.mark.asyncio -async def test_before_reasoning_abort_skips_reasoner_and_commit_and_dispatches(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])), - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace(run_turn=AsyncMock()) - event_bus = EventBus() - dispatch_port = AsyncMock(return_value=True) - - async def abort_handler(ctx): - ctx.abort = True - ctx.abort_reply = "rate limited" - return ctx - - event_bus.on(BeforeReasoningCtx, abort_handler) - - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock( - return_value="telegram:123:0" - ), - ) - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - event_bus=event_bus, - outbound_port=cast(OutboundPort, dispatch_port), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - out = await pipeline.run( - msg, - "telegram:123", - dispatch_outbound=True, - **_chat_models(), - ) - - assert out.content == "rate limited" - assert out.turn_disposition is TurnDisposition.SHORT_CIRCUITED - reasoner.run_turn.assert_not_called() - dispatch_port.dispatch.assert_awaited_once() - dispatched = dispatch_port.dispatch.await_args.args[0] - assert dispatched.content == "rate limited" - - -@pytest.mark.asyncio -async def test_abort_does_not_dispatch_when_dispatch_outbound_false(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])), - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace(run_turn=AsyncMock()) - event_bus = EventBus() - dispatch_port = AsyncMock(return_value=True) - - async def abort_handler(ctx): - ctx.abort = True - ctx.abort_reply = "quiet abort" - return ctx - - event_bus.on(BeforeTurnCtx, abort_handler) - - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - ) - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - event_bus=event_bus, - outbound_port=cast(OutboundPort, dispatch_port), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - out = await pipeline.run(msg, "telegram:123", dispatch_outbound=False) - - assert out.content == "quiet abort" - reasoner.run_turn.assert_not_called() - dispatch_port.dispatch.assert_not_called() - - -@pytest.mark.asyncio -async def test_reasoner_exception_turn_returns_control_outbound(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])), - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace( - run_turn=AsyncMock(side_effect=RuntimeError("budget guard")), - ) - dispatch_port = AsyncMock(return_value=True) - - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="telegram:123:0"), - append_messages=AsyncMock(), - ), - presence=None, - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - outbound_port=cast(OutboundPort, dispatch_port), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - out = await pipeline.run( - msg, - "telegram:123", - dispatch_outbound=True, - **_chat_models(), - ) - - assert out.content == "处理消息时出错,请稍后再试。" - dispatch_port.dispatch.assert_awaited_once() - dispatched = dispatch_port.dispatch.await_args.args[0] - assert dispatched.content == "处理消息时出错,请稍后再试。" - - with pytest.raises(RuntimeError, match="budget guard"): - _ = await pipeline.run( - msg, - "telegram:123", - dispatch_outbound=False, - **_chat_models(), - ) - - -@pytest.mark.asyncio -async def test_after_turn_dispatch_exception_is_not_wrapped_by_control_outbound(): - session = _DummySession("telegram:123") - context_store = SimpleNamespace( - prepare=AsyncMock(return_value=ContextBundle()), - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])), - ) - tools = SimpleNamespace(set_context=MagicMock()) - reasoner = SimpleNamespace( - run_turn=AsyncMock( - return_value=TurnRunResult( - reply="ok", - tools_used=[], - tool_chain=[], - thinking=None, - context_retry={}, - ) - ) - ) - dispatch_port = SimpleNamespace( - dispatch=AsyncMock(side_effect=RuntimeError("dispatch failed")) - ) - - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - SessionServices, - SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="telegram:123:0"), - append_messages=AsyncMock(), - ), - presence=None, - ), - ), - context_store=cast(ContextStore, context_store), - context=cast(ContextBuilder, context), - tools=cast(ToolRegistry, tools), - reasoner=cast(Reasoner, reasoner), - outbound_port=cast(OutboundPort, dispatch_port), - ) - ) - msg = InboundMessage(channel="telegram", sender="hua", chat_id="123", content="hi") - - with pytest.raises(RuntimeError, match="dispatch failed"): - await pipeline.run( - msg, - "telegram:123", - dispatch_outbound=True, - **_chat_models(), - ) - - assert len(session.messages) == 2 - assert session.messages[0]["role"] == "user" - assert session.messages[1]["role"] == "assistant" - assert session.messages[1]["content"] == "ok" - dispatch_port.dispatch.assert_awaited_once() diff --git a/tests/test_agent_core_p7_commit.py b/tests/test_agent_core_p7_commit.py deleted file mode 100644 index d05868077..000000000 --- a/tests/test_agent_core_p7_commit.py +++ /dev/null @@ -1,370 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from agent.core.passive_turn import PassiveTurnDeps, PassiveTurnPipeline -from agent.core.response_parser import parse_response -from agent.core.runtime_support import TurnRunResult -from agent.core.types import ContextBundle -from agent.lifecycle.facade import TurnLifecycle -from agent.lifecycle.types import AfterReasoningCtx -from bootstrap.wiring import wire_turn_lifecycle -from bus.event_bus import EventBus -from bus.events import InboundMessage -from bus.events_lifecycle import TurnCommitted -from tests.model_plugin_fakes import build_test_chat_models - - -class _Provider: - context_window = 8192 - runtime_id = "commit-test" - model = "commit-test" - - -_CHAT_MODELS = build_test_chat_models(_Provider()) - - -class _DummySession: - def __init__(self, key: str) -> None: - self.key = key - self.messages: list[dict[str, object]] = [] - self.metadata: dict[str, object] = {} - self.last_consolidated = 0 - - def get_history(self, max_messages: int = 500) -> list[dict[str, object]]: - return self.messages[-max_messages:] - - def history_units(self, *, after_seq: int = -1) -> tuple[SimpleNamespace, ...]: - return (SimpleNamespace(messages=tuple(self.messages)),) - - def add_message( - self, role: str, content: str, media=None, **kwargs - ) -> dict[str, object]: - msg: dict[str, object] = { - "role": role, - "content": content, - "timestamp": datetime.now().isoformat(), - } - if media: - msg["media"] = list(media) - msg.update(kwargs) - self.messages.append(msg) - return msg - - -@pytest.mark.asyncio -async def test_context_store_commit_persists_commits_and_dispatches(): - order: list[str] = [] - session = _DummySession("telegram:123") - presence = SimpleNamespace( - record_user_message=MagicMock(side_effect=lambda _key: None) - ) - session_manager = SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="telegram:123:0"), - append_messages=AsyncMock( - side_effect=lambda *_args, **_kwargs: order.append("persist") - ), - ) - outbound = SimpleNamespace( - dispatch=AsyncMock( - side_effect=lambda *_args, **_kwargs: order.append("dispatch") or True - ) - ) - event_bus = EventBus() - committed_events: list[TurnCommitted] = [] - - event_bus.on( - TurnCommitted, - lambda event: order.append("committed") or committed_events.append(event), - ) - - context_store = SimpleNamespace( - prepare=AsyncMock( - return_value=ContextBundle( - skill_mentions=["refactor"], - ) - ) - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - return_value=SimpleNamespace(system_prompt="p", messages=[]), - ) - ) - reasoner = SimpleNamespace( - run_turn=AsyncMock( - return_value=TurnRunResult( - reply="整理好了", - tools_used=["noop"], - tool_chain=[{"text": "", "calls": []}], - thinking="思考", - streamed=True, - context_retry={ - "selected_plan": "full", - "react_stats": { - "iteration_count": 3, - "turn_input_sum_tokens": 42100, - "turn_input_peak_tokens": 18800, - "final_call_input_tokens": 17500, - }, - }, - ) - ) - ) - tools = SimpleNamespace(set_context=MagicMock()) - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - Any, - SimpleNamespace( - session_manager=session_manager, - presence=presence, - ), - ), - context_store=cast(Any, context_store), - context=cast(Any, context), - tools=cast(Any, tools), - reasoner=cast(Any, reasoner), - event_bus=event_bus, - outbound_port=cast(Any, outbound), - ) - ) - - out = await pipeline.run( - InboundMessage( - channel="telegram", - sender="hua", - chat_id="123", - content="你好", - metadata={"req_id": "r1"}, - ), - "telegram:123", - chat_models=_CHAT_MODELS, - dispatch_outbound=True, - ) - await event_bus.drain() - - assert out.content == "整理好了" - assert out.media == [] - assert out.metadata["req_id"] == "r1" - assert out.metadata["tools_used"] == ["noop"] - assert out.metadata["streamed_reply"] is True - assert order == ["persist", "committed", "dispatch"] - presence.record_user_message.assert_called_once_with("telegram:123") - session_manager.append_messages.assert_awaited_once() - assert session.messages[-1]["content"] == "整理好了" - assert session.messages[-1]["reasoning_content"] == "思考" - assert session.messages[-1].get("cited_memory_ids", []) == [] - assert len(committed_events) == 1 - tc = committed_events[0] - assert tc.persisted_user_message == "你好" - assert tc.assistant_response == "整理好了" - assert tc.meme_media_count == 0 - assert tc.raw_reply == "整理好了" - assert "history_window" not in tc.post_reply_budget - assert tc.post_reply_budget["history_messages"] == 2 - await event_bus.aclose() - - -def _make_excluded_pipeline( - session: _DummySession, - event_bus: EventBus, -) -> PassiveTurnPipeline: - session_manager = SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value=f"{session.key}:0"), - append_messages=AsyncMock(), - ) - context_store = SimpleNamespace( - prepare=AsyncMock( - return_value=ContextBundle( - skill_mentions=[], - ) - ) - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - return_value=SimpleNamespace(system_prompt="p", messages=[]), - ) - ) - reasoner = SimpleNamespace( - run_turn=AsyncMock( - return_value=TurnRunResult( - reply="ok", - tools_used=[], - tool_chain=[], - context_retry={}, - ) - ) - ) - tools = SimpleNamespace(set_context=MagicMock()) - return PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - Any, - SimpleNamespace( - session_manager=session_manager, - presence=SimpleNamespace(record_user_message=MagicMock()), - ), - ), - context_store=cast(Any, context_store), - context=cast(Any, context), - tools=cast(Any, tools), - reasoner=cast(Any, reasoner), - event_bus=event_bus, - outbound_port=cast(Any, SimpleNamespace(dispatch=AsyncMock())), - ) - ) - - -@pytest.mark.asyncio -async def test_legacy_session_marker_does_not_control_new_turn_effects(): - session = _DummySession("telegram:123") - session.metadata = {"skip_post_memory": True} - event_bus = EventBus() - committed_events: list[TurnCommitted] = [] - event_bus.on(TurnCommitted, committed_events.append) - pipeline = _make_excluded_pipeline(session, event_bus) - - await pipeline.run( - InboundMessage( - channel="telegram", - sender="hua", - chat_id="123", - content="你好", - ), - "telegram:123", - chat_models=_CHAT_MODELS, - dispatch_outbound=True, - ) - await event_bus.drain() - - assert "effects" not in session.messages[0] - assert "effects" not in session.messages[1] - assert len(committed_events) == 1 - assert set(committed_events[0].extra) == {"model_binding"} - - -@pytest.mark.asyncio -async def test_turn_effect_persists_on_both_messages(): - session = _DummySession("telegram:123") - event_bus = EventBus() - pipeline = _make_excluded_pipeline(session, event_bus) - - await pipeline.run( - InboundMessage( - channel="telegram", - sender="hua", - chat_id="123", - content="你好", - metadata={"effects": {"post_commit": "suppress"}}, - ), - "telegram:123", - chat_models=_CHAT_MODELS, - dispatch_outbound=True, - ) - await event_bus.drain() - - assert session.messages[0]["effects"] == {"post_commit": "suppress"} - assert session.messages[1]["effects"] == {"post_commit": "suppress"} - - -@pytest.mark.asyncio -async def test_turn_committed_omits_user_message_when_user_turn_not_persisted(): - session = _DummySession("cli:direct") - session_manager = SimpleNamespace( - get_or_create=MagicMock(return_value=session), - peek_next_message_id=MagicMock(return_value="cli:direct:0"), - append_messages=AsyncMock(), - ) - event_bus = EventBus() - committed_events: list[TurnCommitted] = [] - event_bus.on(TurnCommitted, lambda event: committed_events.append(event)) - - context_store = SimpleNamespace( - prepare=AsyncMock( - return_value=ContextBundle( - skill_mentions=[], - ) - ) - ) - context = SimpleNamespace( - last_debug_breakdown=[], - render=MagicMock( - return_value=SimpleNamespace(system_prompt="p", messages=[]), - ) - ) - reasoner = SimpleNamespace( - run_turn=AsyncMock( - return_value=TurnRunResult( - reply="完成", - tools_used=[], - tool_chain=[], - thinking=None, - streamed=False, - context_retry={}, - ) - ) - ) - pipeline = PassiveTurnPipeline( - PassiveTurnDeps( - session=cast( - Any, - SimpleNamespace( - session_manager=session_manager, - presence=None, - ), - ), - context_store=cast(Any, context_store), - context=cast(Any, context), - tools=cast( - Any, - SimpleNamespace(set_context=MagicMock()), - ), - reasoner=cast(Any, reasoner), - event_bus=event_bus, - outbound_port=cast( - Any, - SimpleNamespace(dispatch=AsyncMock(return_value=True)), - ), - ) - ) - - await pipeline.run( - InboundMessage( - channel="cli", - sender="hua", - chat_id="direct", - content="内部提示词", - metadata={"omit_user_turn": True}, - ), - "cli:direct", - chat_models=_CHAT_MODELS, - dispatch_outbound=False, - ) - await event_bus.drain() - - assert committed_events[0].persisted_user_message is None - assert committed_events[0].assistant_response == "完成" - assert [msg["role"] for msg in session.messages] == ["assistant"] - session_manager.append_messages.assert_awaited_once() - await event_bus.aclose() - - -def test_response_parser_keeps_reply_protocols_for_plugins(): - text = "答复正文\n§cited:[mem_1]§ " - - parsed = parse_response(text, tool_chain=[]) - - assert parsed.clean_text == text - assert parsed.metadata.raw_text == text - - -# ── 新链 (AfterReasoning + AfterTurn) 端到端测试 ── diff --git a/tests/test_agent_loop_contracts.py b/tests/test_agent_loop_contracts.py deleted file mode 100644 index 8fb8dfaa1..000000000 --- a/tests/test_agent_loop_contracts.py +++ /dev/null @@ -1,647 +0,0 @@ -from __future__ import annotations - -import asyncio -from collections.abc import Coroutine -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import AsyncMock - -import pytest - -from agent.control.context import running_turn_id -from agent.plugin_composition.channels import ( - ChannelDeliveryReceipt, - DeliveryStatus as ChannelDeliveryStatus, -) -from agent.looping.core import AgentLoop -from agent.looping.ports import LLMConfig -from agent.looping.session_lane import SessionLaneRegistry -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot -from bus.event_bus import EventBus -from bus.events import ( - InboundItem, - InboundMessage, - OutboundMessage, - TurnTerminalStatus, -) -from bus.events_lifecycle import TurnStarted -from bus.queue import MessageBus -from core.error_context import ( - current_client_message_id, - current_session_key, -) -from session.store import SessionStore -from tests.model_plugin_fakes import build_test_model_store - - -@pytest.mark.asyncio -async def test_runtime_admission_reuses_exact_task_bound_snapshot() -> None: - old_snapshot = SimpleNamespace(snapshot_id="old", tool_registry=None) - lease = SimpleNamespace( - active=True, - snapshot=old_snapshot, - validation_candidate_plugin_ids=frozenset(), - ) - store = SimpleNamespace( - current=SimpleNamespace(snapshot_id="new"), - acquire=AsyncMock(side_effect=AssertionError("must not reacquire current")), - ) - loop = AgentLoop.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = store - - async def process(_item: object, **_kwargs: object) -> str: - from agent.plugins.snapshot import get_current_runtime_snapshot - - snapshot = get_current_runtime_snapshot() - assert snapshot is old_snapshot - return snapshot.snapshot_id - - loop._process = process - item = SimpleNamespace(session_key="feishu:chat") - token = bind_runtime_snapshot(cast(Any, lease)) - try: - result = await loop._process_with_runtime_admission(cast(Any, item)) - finally: - reset_runtime_snapshot(token) - - assert result == "old" - store.acquire.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_run_cleans_active_state_before_inbound_completion_failure() -> None: - item = InboundMessage( - channel="cli", - sender="user", - chat_id="1", - content="hello", - ) - bus = SimpleNamespace( - consume_inbound=AsyncMock(return_value=item), - complete_inbound=AsyncMock(side_effect=RuntimeError("ack failed")), - ) - loop = AgentLoop.__new__(AgentLoop) - loop._llm_config = LLMConfig() - loop.bus = bus - loop._active_tasks = {} - loop._active_turn_states = {} - loop._process_with_runtime_admission = AsyncMock( - return_value=OutboundMessage(channel="cli", chat_id="1", content="ok") - ) - - with pytest.raises(RuntimeError, match="ack failed"): - await loop.run() - - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -@pytest.mark.asyncio -async def test_run_propagates_runtime_cancellation_after_ack() -> None: - item = InboundMessage( - channel="cli", - sender="user", - chat_id="1", - content="hello", - ) - consumed = False - started = asyncio.Event() - - async def consume_inbound() -> InboundMessage: - nonlocal consumed - if consumed: - raise AssertionError("运行器取消后不应继续消费消息") - consumed = True - return item - - async def process( - _item: InboundMessage, - *, - execution_turn_id: str | None = None, - ) -> OutboundMessage: - started.set() - await asyncio.Future() - raise AssertionError("unreachable") - - complete_inbound = AsyncMock() - loop = AgentLoop.__new__(AgentLoop) - loop._llm_config = LLMConfig() - loop.bus = SimpleNamespace( - consume_inbound=consume_inbound, - complete_inbound=complete_inbound, - ) - loop._active_tasks = {} - loop._active_turn_states = {} - loop._process_with_runtime_admission = process - - run_task = asyncio.create_task(loop.run()) - await started.wait() - run_task.cancel() - - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(run_task, timeout=0.5) - - complete_inbound.assert_awaited_once_with(item) - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - assert loop._running is False - - -@pytest.mark.asyncio -async def test_run_waits_for_ack_before_propagating_runtime_cancellation() -> None: - item = InboundMessage( - channel="cli", - sender="user", - chat_id="1", - content="hello", - ) - consumed = False - started = asyncio.Event() - ack_started = asyncio.Event() - release_ack = asyncio.Event() - - async def consume_inbound() -> InboundMessage: - nonlocal consumed - if consumed: - raise AssertionError("运行器取消后不应继续消费消息") - consumed = True - return item - - async def process( - _item: InboundMessage, - *, - execution_turn_id: str | None = None, - ) -> OutboundMessage: - started.set() - await asyncio.Future() - raise AssertionError("unreachable") - - async def complete_inbound(_item: InboundMessage) -> None: - ack_started.set() - await release_ack.wait() - - loop = AgentLoop.__new__(AgentLoop) - loop._llm_config = LLMConfig() - loop.bus = SimpleNamespace( - consume_inbound=consume_inbound, - complete_inbound=complete_inbound, - ) - loop._active_tasks = {} - loop._active_turn_states = {} - loop._process_with_runtime_admission = process - - run_task = asyncio.create_task(loop.run()) - await started.wait() - run_task.cancel() - await ack_started.wait() - assert run_task.done() is False - - release_ack.set() - with pytest.raises(asyncio.CancelledError): - await asyncio.wait_for(run_task, timeout=0.5) - - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -@pytest.mark.asyncio -async def test_stop_cancels_active_turn_and_acknowledges_inbound() -> None: - item = InboundMessage( - channel="cli", - sender="user", - chat_id="1", - content="hello", - ) - started = asyncio.Event() - cancelled = asyncio.Event() - - async def process( - _item: InboundMessage, - *, - execution_turn_id: str | None = None, - ) -> OutboundMessage: - started.set() - try: - await asyncio.Future() - finally: - cancelled.set() - raise AssertionError("unreachable") - - bus = SimpleNamespace( - consume_inbound=AsyncMock(return_value=item), - complete_inbound=AsyncMock(), - ) - loop = AgentLoop.__new__(AgentLoop) - loop._llm_config = LLMConfig() - loop.bus = bus - loop._active_tasks = {} - loop._active_turn_states = {} - loop._process_with_runtime_admission = process - - run_task = asyncio.create_task(loop.run()) - await started.wait() - loop.stop() - await asyncio.wait_for(run_task, timeout=0.5) - - assert cancelled.is_set() - bus.complete_inbound.assert_awaited_once_with(item) - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -def _real_path_loop( - bus: Any, - core_process: object, -) -> AgentLoop: - """最小化真实执行链脚手架:真实 _run_inbound_turn / _process_with_runtime_admission / _process, - 只替换 _react 与总线/事件观察点。""" - loop = AgentLoop.__new__(AgentLoop) - loop._llm_config = LLMConfig() - loop._session_services = SimpleNamespace( - session_manager=SimpleNamespace( - get_or_create=lambda _key: SimpleNamespace(metadata={}), - ) - ) - loop.bus = bus - loop._event_bus = EventBus() - loop.tools = SimpleNamespace(get_tool=lambda _name: None) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = build_test_model_store(object()) - loop._passive_pipeline = SimpleNamespace( - run_command=AsyncMock(return_value=None), - ) - - async def react( - message: InboundMessage, - key: str, - *, - chat_models: object, - model_id: str | None, - reasoning_effort: str | None, - dispatch_outbound: bool, - command_admitted: bool, - ) -> OutboundMessage: - _ = command_admitted, chat_models, model_id, reasoning_effort - return await core_process( # type: ignore[operator] - message, - key, - dispatch_outbound=dispatch_outbound, - ) - - loop._react = react - loop._outbound_port = SimpleNamespace( - dispatch=AsyncMock( - return_value=ChannelDeliveryReceipt( - delivery_id="test-delivery", - status=ChannelDeliveryStatus.DELIVERED, - ) - ) - ) - loop._processing_state = None - loop._active_tasks = {} - loop._active_turn_states = {} - return loop - - -@pytest.mark.asyncio -async def test_error_final_carries_authoritative_execution_turn_id() -> None: - """真实 child 链:child 捕获 execution ID 后抛错,parent 错误 final 同源发布。""" - item = InboundMessage( - channel="mobile", - sender="user", - chat_id="chat-a", - content="hello", - metadata={"display_content": "继续说明"}, - ) - bus = SimpleNamespace( - complete_inbound=AsyncMock(), - ) - observed_child_turn_ids: list[str] = [] - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - observed_child_turn_ids.append(running_turn_id.get()) - assert running_turn_id.get().startswith("turn:") - raise RuntimeError("boom") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - await loop._run_inbound_turn(item) - - (outbound,) = loop._outbound_port.dispatch.call_args.args - assert outbound.content == "出错:boom" - assert outbound.control_turn_id == observed_child_turn_ids[0] - assert outbound.control_turn_id.startswith("turn:") - assert started_events[0].turn_id == observed_child_turn_ids[0] - assert started_events[0].content == "继续说明" - bus.complete_inbound.assert_awaited_once_with(item) - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -@pytest.mark.asyncio -async def test_error_final_preserves_preprovided_execution_turn_id() -> None: - """预提供 execution ID 原样贯通;与 control_turn_id 分叉时 execution 恒为 owner。""" - item = InboundMessage( - channel="mobile", - sender="user", - chat_id="chat-a", - content="hello", - metadata={ - "_control_execution_turn_id": "turn:pre", - "control_turn_id": "interaction:1", - }, - ) - bus = SimpleNamespace( - complete_inbound=AsyncMock(), - ) - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - assert running_turn_id.get() == "turn:pre" - raise RuntimeError("boom") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - await loop._run_inbound_turn(item) - - (outbound,) = loop._outbound_port.dispatch.call_args.args - assert outbound.control_turn_id == "interaction:1" - assert outbound.execution_attempt_id == "turn:pre" - assert outbound.terminal_status is TurnTerminalStatus.FAILED - assert started_events[0].turn_id == "turn:pre" - bus.complete_inbound.assert_awaited_once_with(item) - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -@pytest.mark.asyncio -async def test_error_final_preserves_control_turn_id_only_metadata() -> None: - """只有 control_turn_id 的 direct-call/恢复合同:原样保留为该轮 owner。""" - item = InboundMessage( - channel="mobile", - sender="user", - chat_id="chat-a", - content="hello", - metadata={"control_turn_id": "turn:ctrl"}, - ) - bus = SimpleNamespace( - complete_inbound=AsyncMock(), - ) - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - assert running_turn_id.get() == "turn:ctrl" - raise RuntimeError("boom") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - await loop._run_inbound_turn(item) - - (outbound,) = loop._outbound_port.dispatch.call_args.args - assert outbound.control_turn_id == "turn:ctrl" - assert started_events[0].turn_id == "turn:ctrl" - bus.complete_inbound.assert_awaited_once_with(item) - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - - -@pytest.mark.asyncio -async def test_non_str_control_turn_id_fails_loud_without_polluting_owner_maps() -> ( - None -): - """非字符串 control_turn_id 在入站边界 fail-loud:TypeError 原样抛出、 - active maps 不被污染、无 TurnStarted、无 error final;execution owner - 尚未建立,绝不确认——durable handoff 保留供下一次恢复。""" - item = InboundMessage( - channel="mobile", - sender="user", - chat_id="chat-a", - content="hello", - metadata={"control_turn_id": 7}, - ) - bus = SimpleNamespace( - complete_inbound=AsyncMock(), - ) - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - raise AssertionError("边界校验失败不应进入核心处理") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - with pytest.raises(TypeError): - await loop._run_inbound_turn(item) - - assert started_events == [] - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - loop._outbound_port.dispatch.assert_not_awaited() - bus.complete_inbound.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_durable_poison_inbound_not_acked_and_offered_to_next_recovery( - tmp_path: Path, -) -> None: - """真实 MessageBus + SessionStore:边界失败绝不确认删除 durable handoff, - 记录仍存在、仍被 durable owner 持有。同进程恢复不复制;模拟重启后由同一 - DB 新建的 SessionStore/MessageBus 重放同一 handoff 再次处理,recover 两次 - 仍只入队一次、唯一 accepted owner、handoff id 相同,第二次边界失败后 - row 仍存在。""" - db_path = tmp_path / "sessions.db" - store = SessionStore(db_path) - bus = MessageBus() - bus.bind_durable_inbound_store(store) - item = InboundMessage( - channel="akashic", - sender="device:1", - chat_id="chat-a", - content="hello", - metadata={"client_message_id": "client-poison", "control_turn_id": 7}, - handoff_id="handoff-client-poison", - ) - await bus.publish_inbound(item) - consumed = await bus.consume_inbound() - assert consumed.handoff_id is not None - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - raise AssertionError("边界校验失败不应进入核心处理") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - with pytest.raises(TypeError): - await loop._run_inbound_turn(cast(InboundItem, consumed)) - - assert started_events == [] - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - rows = store.list_inbound_handoffs() - assert [row["handoff_id"] for row in rows] == [consumed.handoff_id] - assert bus.has_pending_mobile_handoff( - session_key=consumed.session_key, - client_message_id="client-poison", - ) - assert id(consumed) in bus._inbound_accepted - - # 同进程恢复不得复制:poison 仍被 accepted owner 持有,整页重放直接跳过。 - await bus.recover_durable_inbounds() - assert bus.inbound_size == 0 - assert [row["handoff_id"] for row in store.list_inbound_handoffs()] == [ - consumed.handoff_id - ] - - # 模拟重启:遗弃旧 bus/store,用同一 DB 新建 SessionStore 与 MessageBus。 - await bus.aclose() - store.close() - restarted_store = SessionStore(db_path) - restarted = MessageBus() - restarted.bind_durable_inbound_store(restarted_store) - await restarted.recover_durable_inbounds() - await restarted.recover_durable_inbounds() - assert restarted.inbound_size == 1 - owners = [ - owner.item - for owner in restarted._inbound_accepted.values() - if isinstance(owner.item, InboundMessage) and owner.item.handoff_id is not None - ] - assert [owner.handoff_id for owner in owners] == [consumed.handoff_id] - - recovered = await restarted.consume_inbound() - assert recovered.handoff_id == consumed.handoff_id - assert recovered.metadata["control_turn_id"] == 7 - - restarted_loop = _real_path_loop(restarted, core_process) - restarted_loop._event_bus.on(TurnStarted, started_events.append) - with pytest.raises(TypeError): - await restarted_loop._run_inbound_turn(cast(InboundItem, recovered)) - assert started_events == [] - assert restarted_loop._active_tasks == {} - assert restarted_loop._active_turn_states == {} - assert [row["handoff_id"] for row in restarted_store.list_inbound_handoffs()] == [ - consumed.handoff_id - ] - assert id(recovered) in restarted._inbound_accepted - await restarted.aclose() - restarted_store.close() - - -@pytest.mark.asyncio -async def test_owner_task_creation_failure_never_acks_and_leaves_no_maps( - monkeypatch: pytest.MonkeyPatch, -) -> None: - """child task 建立失败:绝不确认、不发布 outbound、不残留任何 active map。""" - item = InboundMessage( - channel="mobile", - sender="user", - chat_id="chat-a", - content="hello", - ) - bus = SimpleNamespace( - complete_inbound=AsyncMock(), - ) - real_create_task = asyncio.create_task - - def failing_create( - coro: Coroutine[Any, Any, Any], - *, - name: str | None = None, - ): - if name is not None and name.startswith("agent-turn:"): - coro.close() - raise RuntimeError("task create failed") - return real_create_task(coro, name=name) - - monkeypatch.setattr(asyncio, "create_task", failing_create) - loop = _real_path_loop(bus, object()) - - with pytest.raises(RuntimeError, match="task create failed"): - await loop._run_inbound_turn(item) - - assert loop._active_tasks == {} - assert loop._active_turn_states == {} - loop._outbound_port.dispatch.assert_not_awaited() - bus.complete_inbound.assert_not_awaited() - - -@pytest.mark.asyncio -async def test_process_direct_message_real_entry_execution_owner() -> None: - """公开入口 process_direct_message:execution turn id 恒为 owner,与 - interaction 分组 id 分叉时 execution 胜出,child 与 TurnStarted 同源。""" - bus = SimpleNamespace() - observed_child_turn_ids: list[str] = [] - started_events: list[TurnStarted] = [] - - async def core_process( - _msg: InboundMessage, - _key: str, - *, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - observed_child_turn_ids.append(running_turn_id.get()) - raise RuntimeError("boom") - - loop = _real_path_loop(bus, core_process) - loop._event_bus.on(TurnStarted, started_events.append) - - with pytest.raises(RuntimeError, match="boom"): - await loop.process_direct_message( - "hello", - turn_id="turn:owner", - interaction_id="interaction:1", - ) - - assert observed_child_turn_ids == ["turn:owner"] - assert started_events[0].turn_id == "turn:owner" - - -@pytest.mark.asyncio -async def test_process_direct_message_non_str_metadata_fails_loud_clean() -> None: - """公开入口非字符串 metadata:TypeError 原样抛出,不发布 outbound, - 不泄漏任何 contextvar(零副作用)。""" - bus = SimpleNamespace() - loop = _real_path_loop(bus, object()) - - with pytest.raises(TypeError): - await loop.process_direct_message( - "hello", - metadata={"control_turn_id": 7}, - ) - - loop._outbound_port.dispatch.assert_not_awaited() - assert current_session_key.get() is None - assert running_turn_id.get() == "" - assert current_client_message_id.get() == "" diff --git a/tests/test_agent_self_check.py b/tests/test_agent_self_check.py deleted file mode 100644 index 1d29f8b9b..000000000 --- a/tests/test_agent_self_check.py +++ /dev/null @@ -1,48 +0,0 @@ -from datetime import datetime, timedelta -from pathlib import Path -from typing import cast -from unittest.mock import MagicMock - -import pytest -from agent.core.passive_support import collect_skill_mentions -from agent.core.passive_turn import DefaultReasoner -from prompts.agent import build_current_message_time_envelope - - -def test_collect_skill_mentions_returns_unique_existing_names(tmp_path): - skills = [ - "feed-manage", - "refactor", - ] - - got = collect_skill_mentions( - "请用 $feed-manage 然后 $refactor 再来一次 $feed-manage", - skills, - ) - - assert got == ["feed-manage", "refactor"] - - -def test_collect_skill_mentions_ignores_unknown_skill(tmp_path): - skills = ["known"] - - got = collect_skill_mentions("$known $unknown", skills) - - assert got == ["known"] - - -def test_format_request_time_anchor_contains_iso_and_label(): - text = DefaultReasoner.format_request_time_anchor(None) - assert text.startswith("request_time=") - assert "(" in text and ")" in text - - -def test_build_current_message_time_envelope_contains_today_and_tomorrow(): - message_timestamp = datetime.fromisoformat("2026-04-08T17:57:00+08:00") - local_timestamp = message_timestamp.astimezone() - - text = build_current_message_time_envelope(message_timestamp=message_timestamp) - - assert f"当前消息时间: {local_timestamp:%Y-%m-%d %H:%M}" in text - assert f"今天={local_timestamp:%Y-%m-%d}" in text - assert f"明天={local_timestamp + timedelta(days=1):%Y-%m-%d}" in text diff --git a/tests/test_akasha_inspector_sidecar.py b/tests/test_akasha_inspector_sidecar.py deleted file mode 100644 index a5c169266..000000000 --- a/tests/test_akasha_inspector_sidecar.py +++ /dev/null @@ -1,231 +0,0 @@ -from __future__ import annotations - -import json -import sqlite3 -from contextlib import closing -from pathlib import Path - -import pytest - -from agent.tools.recall_memory import render_memory_unavailable -from plugins.akasha.config import AkashaConfig -from plugins.akasha.infrastructure.sparse_index.builder import ( - AppendOnlyViolation, - BuildConfig, - build_sparse_index, -) -from plugins.akasha.infrastructure.sparse_index.schema import SCHEMA -from plugins.akasha.inspector import AkashaInspectorReader, _tool_recall_lanes - - -def _create_source(path: Path, tool_chain: str | None) -> None: - connection = sqlite3.connect(path) - connection.executescript(""" - CREATE TABLE sessions ( - key TEXT PRIMARY KEY, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_consolidated INTEGER NOT NULL DEFAULT 0, - metadata TEXT - ); - CREATE TABLE messages ( - id TEXT PRIMARY KEY, - session_key TEXT NOT NULL, - seq INTEGER NOT NULL, - role TEXT NOT NULL, - content TEXT, - tool_chain TEXT, - extra TEXT, - ts TEXT NOT NULL, - UNIQUE(session_key, seq) - ); - CREATE TABLE message_embeddings ( - message_id TEXT NOT NULL, - content_hash TEXT NOT NULL, - model TEXT NOT NULL, - embedding BLOB NOT NULL, - dim INTEGER NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY(message_id, model) - ); - """) - connection.execute( - "INSERT INTO sessions VALUES (?, ?, ?, 0, NULL)", - ( - "test:one", - "2026-08-17T00:00:00+00:00", - "2026-08-17T00:00:01+00:00", - ), - ) - connection.executemany( - "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [ - ( - "user:0", - "test:one", - 0, - "user", - "hello", - None, - None, - "2026-08-17T00:00:00+00:00", - ), - ( - "assistant:1", - "test:one", - 1, - "assistant", - "answer", - tool_chain, - None, - "2026-08-17T00:00:01+00:00", - ), - ], - ) - connection.commit() - connection.close() - - -def _build_source_sidecar(tmp_path: Path, tool_chain: str | None) -> Path: - source = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_source(source, tool_chain) - build_sparse_index(source, index, BuildConfig()) - return index - - -def _reader(tmp_path: Path, index: Path) -> AkashaInspectorReader: - memory = tmp_path / "akasha.db" - sqlite3.connect(memory).close() - return AkashaInspectorReader( - memory_root=tmp_path, - config=AkashaConfig(db_path="akasha.db", index_path=index.name), - ) - - -def test_sparse_projection_preserves_tool_chain_without_sessions_attach( - tmp_path: Path, -) -> None: - chain = json.dumps( - [ - { - "calls": [ - { - "name": "recall_memory", - "status": "success", - "result": render_memory_unavailable("embedding unavailable"), - } - ] - } - ] - ) - index = _build_source_sidecar(tmp_path, chain) - (tmp_path / "sessions.db").unlink() - reader = _reader(tmp_path, index) - - with closing(reader._connect()) as connection: # noqa: SLF001 - databases = {str(row[1]) for row in connection.execute("PRAGMA database_list")} - projected = connection.execute( - "SELECT assistant_tool_chain_json FROM sparse.sparse_turns" - ).fetchone()[0] - - assert databases == {"main", "sparse"} - assert projected == chain - assert _tool_recall_lanes(projected) == ([], []) - - -def test_source_tool_chain_change_is_not_accepted_as_incremental_append( - tmp_path: Path, -) -> None: - index = _build_source_sidecar(tmp_path, "[]") - source = tmp_path / "sessions.db" - connection = sqlite3.connect(source) - connection.execute( - "UPDATE messages SET tool_chain = ? WHERE id = 'assistant:1'", - (json.dumps([{"calls": [{"name": "shell"}]}]),), - ) - connection.commit() - connection.close() - - with pytest.raises(AppendOnlyViolation, match="indexed turn changed"): - build_sparse_index(source, index, BuildConfig()) - - -def test_old_sparse_schema_fails_loud_with_rebuild_instruction( - tmp_path: Path, -) -> None: - index = tmp_path / "index.db" - connection = sqlite3.connect(index) - connection.executescript( - SCHEMA.replace(" assistant_tool_chain_json TEXT,\n", "") - ) - connection.execute("INSERT INTO metadata(key, value) VALUES ('index_version', '9')") - connection.commit() - connection.close() - reader = _reader(tmp_path, index) - - with pytest.raises(ValueError, match="explicit rebuild is required"): - with closing(reader._connect()): # noqa: SLF001 - pass - - -def test_inspector_rejects_sidecars_outside_declared_memory_root( - tmp_path: Path, -) -> None: - with pytest.raises(ValueError, match="必须位于 memory root"): - AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=AkashaConfig( - db_path="../sessions.db", - index_path="memory/akasha-v2-index.db", - ), - ) - - -def test_engine_and_inspector_share_declared_memory_path_contract( - tmp_path: Path, -) -> None: - memory_root = tmp_path / "memory" - - direct = AkashaInspectorReader( - memory_root=memory_root, - config=AkashaConfig( - db_path="akasha.db", - index_path="akasha-v2-index.db", - ), - ) - historical = AkashaInspectorReader( - memory_root=memory_root, - config=AkashaConfig(), - ) - - assert direct.paths == historical.paths - assert direct.paths.memory == memory_root / "akasha.db" - assert direct.paths.index == memory_root / "akasha-v2-index.db" - with pytest.raises(ValueError, match="必须位于 memory root"): - AkashaInspectorReader( - memory_root=memory_root, - config=AkashaConfig( - db_path="custom/akasha.db", - index_path="akasha-v2-index.db", - ), - ) - - -def test_inspector_rejects_symlinked_memory_root( - tmp_path: Path, -) -> None: - outside = tmp_path / "outside" - outside.mkdir() - memory_root = tmp_path / "workspace" / "memory" - memory_root.parent.mkdir() - memory_root.symlink_to(outside, target_is_directory=True) - - with pytest.raises(ValueError, match="memory root 不能是符号链接"): - AkashaInspectorReader( - memory_root=memory_root, - config=AkashaConfig(), - ) - - assert list(outside.iterdir()) == [] diff --git a/tests/test_akasha_plugin.py b/tests/test_akasha_plugin.py deleted file mode 100644 index 998729417..000000000 --- a/tests/test_akasha_plugin.py +++ /dev/null @@ -1,4185 +0,0 @@ -from __future__ import annotations - -import asyncio -import hashlib -import json -import logging -import math -import os -import sqlite3 -import struct -import threading -from contextlib import asynccontextmanager, closing -from dataclasses import replace -from datetime import datetime, timedelta, timezone -from pathlib import Path -from typing import Any, Mapping, Sequence, cast - -import numpy as np -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from agent.config_models import ( - Config, -) -from agent.control.context import running_turn_id -from agent.plugin_composition import ( - EMBEDDINGS, - EMBEDDING_MEMORY_PLUGIN, - TOOL_CATALOG, - CompositionError, - CompositionRoot, - DashboardContext, - EmbeddingResult, - EmbeddingSpaceDescriptor, - PluginTools, - PluginRuntime, -) -from agent.plugin_composition.tool_catalog import _freeze_plugin_tools -from agent.plugins.composable import ComposablePlugin -from agent.plugins.mobile_ui import _normalize_rpc_result -from agent.plugins.manifest import ( - builtin_plugin_data_dir, - ensure_workspace_plugin_data_dir, -) -from agent.tools.base import ToolExecutionContext, tool_execution_context_scope -from agent.tools.recall_memory import RecallMemoryTool -from bus.event_bus import EventBus -from bus.events_lifecycle import TurnCommitted -from core.error_context import current_client_message_id, current_session_key -from core.memory.engine import MemoryQuery, MemoryQueryResult, MemoryScope -from plugins.akasha.application.cycle import MemoryCycle -from plugins.akasha.application.rebuild import rebuild_memory -from plugins.akasha.application.runtime import OnlineMemoryRuntime -from plugins.akasha.config import AkashaConfig, load_akasha_config -from plugins.akasha.dashboard import register as register_dashboard -from plugins.akasha.domain import features as features_module -from plugins.akasha.domain.features import BurstAwareFeaturePool -from plugins.akasha.domain.model import MemoryConfig -from plugins.akasha.engine import ( - ActiveRecallSnapshot, - AkashaMemoryEngine, - PendingRetrieval, - RetrievalRecords, -) -from plugins.akasha.inspector import AkashaInspectorReader, mobile_summary -from plugins.akasha.repair import ReindexRequest, reindex -from plugins.akasha import repair as repair_module -from plugins.akasha.infrastructure.loader import load_turn_suffix, load_turns -from plugins.akasha.infrastructure.persistence import ( - logical_state_sha256, -) -from plugins.akasha.infrastructure.sparse_index import ( - BuildConfig, - SparseIndexRebuildRequired, - audit_source_embeddings, - build_sparse_index, - sparse_index_state_sha256, -) -from plugins.akasha.infrastructure.sparse_index.schema import ( - INDEX_VERSION, - SCHEMA, - TOOL_CHAIN_PROJECTION_VERSION, -) -from plugins.akasha import plugin as akasha_plugin -from plugins.akasha.plugin import ( - _AkashaMobileQuery, - _empty_mobile_recall, - _mobile_recall_lane, -) -from agent.plugin_composition import ServiceKey - -MEMORY_RECALL = ServiceKey[object]("memory.recall.v1") -from session.store import InteractionDeletion, SessionStore - - -@asynccontextmanager -async def _runtime_scope(): - yield - - -def _embedding_space() -> EmbeddingSpaceDescriptor: - return EmbeddingSpaceDescriptor( - plugin_snapshot_id="test-snapshot", - model_revision=1, - model_id="embedding-model", - connection_id="test-connection", - driver_id="test-driver", - driver_contract_version="1", - auth_identity="test-account", - connection_fingerprint="test-endpoint", - model="embedding-model", - dimensions=2, - normalization="none", - capability_digest="test-capabilities", - ) - - -TEST_EMBEDDING_IDENTITY = _embedding_space().identity - - -@pytest.mark.asyncio -async def test_explicit_reindex_backs_up_and_publishes_descriptor_space( - tmp_path: Path, -) -> None: - """Repair creates new-space rows and sidecars only after a recoverable backup.""" - - sessions = tmp_path / "sessions.db" - _create_sessions(sessions) - _append_turn( - sessions, - sequence=0, - user="alpha", - assistant="answer", - started=datetime(2026, 8, 29, tzinfo=timezone.utc), - ) - descriptor = _embedding_space() - result = await reindex( - embeddings=_Embeddings(descriptor), - descriptor=descriptor, - request=ReindexRequest( - embedding_identity=descriptor.identity, - model_id=descriptor.model_id, - dimensions=descriptor.dimensions, - requested_at="2026-08-29T00:00:00+00:00", - ), - workspace=tmp_path, - data_root=tmp_path / "plugin-data", - config=AkashaConfig(), - runtime_scope=_runtime_scope, - ) - - assert result.embedded_messages == 2 - assert result.eligible_messages == 2 - assert (result.backup_dir / "sessions-before.db").is_file() - index = tmp_path / "memory" / "akasha-v2-index.db" - with closing(sqlite3.connect(index)) as connection: - identity = connection.execute( - "SELECT value FROM metadata WHERE key = 'embedding_model'" - ).fetchone() - assert identity == (descriptor.identity,) - - -class _BoundEmbedding: - def __init__(self, descriptor: EmbeddingSpaceDescriptor) -> None: - self.descriptor = descriptor - - async def embed(self, texts: Sequence[str]) -> EmbeddingResult: - return EmbeddingResult( - vectors=tuple( - (1.0, 0.0) if "alpha" in text else (0.0, 1.0) for text in texts - ) - ) - - -class _Embeddings: - def __init__(self, descriptor: EmbeddingSpaceDescriptor | None = None) -> None: - self.descriptor = descriptor or _embedding_space() - self.bound = _BoundEmbedding(self.descriptor) - - def describe(self, *, model_id: str | None = None) -> EmbeddingSpaceDescriptor: - if model_id is not None and model_id != self.descriptor.model_id: - raise RuntimeError("test embedding selection conflict") - return self.descriptor - - @asynccontextmanager - async def bind(self, *, model_id: str | None = None): - if model_id is not None and model_id != self.descriptor.model_id: - raise RuntimeError("test embedding selection conflict") - yield self.bound - - -@pytest.mark.asyncio -async def test_explicit_reindex_restores_old_sidecars_when_publish_fails( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """A failed two-file publish leaves the previously readable pair in place.""" - - sessions = tmp_path / "sessions.db" - _create_sessions(sessions) - descriptor = _embedding_space() - request = ReindexRequest( - embedding_identity=descriptor.identity, - model_id=descriptor.model_id, - dimensions=descriptor.dimensions, - requested_at="2026-08-29T00:00:00+00:00", - ) - _append_turn( - sessions, - sequence=0, - user="alpha", - assistant="first", - started=datetime(2026, 8, 29, tzinfo=timezone.utc), - ) - await reindex( - embeddings=_Embeddings(descriptor), - descriptor=descriptor, - request=request, - workspace=tmp_path, - data_root=tmp_path / "plugin-data", - config=AkashaConfig(), - runtime_scope=_runtime_scope, - ) - index = tmp_path / "memory" / "akasha-v2-index.db" - memory = tmp_path / "memory" / "akasha.db" - assert len(load_turns(index)) == 1 - - _append_turn( - sessions, - sequence=2, - user="beta", - assistant="second", - started=datetime(2026, 8, 29, 0, 1, tzinfo=timezone.utc), - ) - original_replace = repair_module.os.replace - - def fail_index_publish(source: Path, destination: Path) -> None: - if ".candidate" in source.name and destination == index: - raise OSError("injected index publish failure") - original_replace(source, destination) - - monkeypatch.setattr(repair_module.os, "replace", fail_index_publish) - with pytest.raises(OSError, match="injected index publish failure"): - await reindex( - embeddings=_Embeddings(descriptor), - descriptor=descriptor, - request=request, - workspace=tmp_path, - data_root=tmp_path / "plugin-data", - config=AkashaConfig(), - runtime_scope=_runtime_scope, - ) - - assert len(load_turns(index)) == 1 - repair_module._validate_sidecars(index, memory, config=AkashaConfig()) - - -def test_akasha_registers_v3_namespace() -> None: - plugin = ComposablePlugin.from_module(akasha_plugin) - - assert plugin.api_version == 3 - assert plugin.name == "akasha" - assert plugin.dashboard_module == "dashboard.py" - assert plugin.workspace_roots == ("memory",) - assert EMBEDDINGS in plugin.inject - - -def test_akasha_source_does_not_read_markdown_profiles() -> None: - plugin_root = Path(akasha_plugin.__file__).parent - source = "\n".join( - path.read_text(encoding="utf-8") for path in plugin_root.glob("*.py") - ) - assert '"MEMORY.md"' not in source - assert '"SELF.md"' not in source - assert "'MEMORY.md'" not in source - assert "'SELF.md'" not in source - - -@pytest.mark.asyncio -async def test_akasha_binds_only_recall_to_memory_recall_service( - tmp_path: Path, -) -> None: - root = CompositionRoot("akasha-tool-contract") - tools = PluginTools(root.instance_token) - _ = await root.context.provide(TOOL_CATALOG, tools) - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - runtime = _runtime_handle(engine) - - async def apply(ctx) -> None: - _ = await ctx.provide(EMBEDDING_MEMORY_PLUGIN, object()) - _ = await ctx.provide(MEMORY_RECALL, object()) - await akasha_plugin._register_tools(ctx, runtime) - - _ = await root.mount( - apply, - name="akasha", - inject=(TOOL_CATALOG,), - runtime=PluginRuntime( - plugin_id="akasha", - generation_id="akasha:generation", - plugin_dir=Path("plugins/akasha"), - data_dir=tmp_path / "plugin-data", - workspace=tmp_path, - config=None, - ), - ) - catalog = _freeze_plugin_tools( - tools, - root.instance_token, - {"akasha": "akasha:generation"}, - root.plugin_service_owners(), - ) - - assert catalog.from_provide(MEMORY_RECALL).definition.name == "recall_memory" - with pytest.raises(CompositionError) as raised: - _ = catalog.from_provide(EMBEDDING_MEMORY_PLUGIN) - assert raised.value.code == "PROVIDED_TOOL_NOT_BOUND" - assert {binding.definition.name for binding in catalog.values()} == { - "recall_memory", - "remember_memory", - "forget_memory", - } - await root.dispose() - await akasha_plugin._close_owned(engine.closeables) - - -def test_engine_and_inspector_resolve_sidecars_from_same_memory_root( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Keep engine writes and inspector reads on the same declared root.""" - - # 1. Construct the real engine with the direct filename syntax. - _create_sessions(tmp_path / "sessions.db") - config = AkashaConfig( - db_path="akasha.db", - index_path="akasha-v2-index.db", - ) - engine = AkashaMemoryEngine( - embeddings=_Embeddings(), - embedding_space=_embedding_space(), - runtime_scope=_runtime_scope, - akasha_config=config, - workspace=tmp_path, - event_publisher=None, - ) - - # 2. Resolve the same config through the dashboard/mobile inspector. - inspector = AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=config, - ) - try: - assert engine._runtime.memory_path == inspector.paths.memory # noqa: SLF001 - assert engine._runtime.index_path == inspector.paths.index # noqa: SLF001 - assert inspector.paths.memory == tmp_path / "memory" / "akasha.db" - assert inspector.paths.index == tmp_path / "memory" / "akasha-v2-index.db" - finally: - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -def test_engine_rejects_markdown_profile_config_before_opening_it( - tmp_path: Path, -) -> None: - _create_sessions(tmp_path / "sessions.db") - memory_dir = tmp_path / "memory" - memory_dir.mkdir() - profile = memory_dir / "MEMORY.md" - profile.write_text("# 用户长期记忆\n", encoding="utf-8") - - with pytest.raises(ValueError, match="不能消费 Markdown profile"): - AkashaMemoryEngine( - embeddings=_Embeddings(), - embedding_space=_embedding_space(), - runtime_scope=_runtime_scope, - akasha_config=AkashaConfig(db_path="memory/MEMORY.md"), - workspace=tmp_path, - event_publisher=None, - ) - - assert profile.read_text(encoding="utf-8") == "# 用户长期记忆\n" - - -def test_engine_refuses_to_relabel_an_existing_embedding_space(tmp_path: Path) -> None: - """保持稳定向量身份,不静默混用旧数据。""" - - sessions = tmp_path / "sessions.db" - index = tmp_path / "memory" / "akasha-v2-index.db" - _create_sessions(sessions) - _append_turn( - sessions, - sequence=0, - user="alpha", - assistant="answer", - started=datetime(2026, 7, 6, tzinfo=timezone.utc), - with_embeddings=True, - ) - build_sparse_index( - sessions, - index, - BuildConfig(embedding_model="embedding-model", embedding_dimension=2), - ) - - with pytest.raises(RuntimeError, match="空间已变化"): - _engine(tmp_path) - - with closing(sqlite3.connect(index)) as connection: - persisted = connection.execute( - "SELECT value FROM metadata WHERE key='embedding_model'" - ).fetchone() - assert persisted == ("embedding-model",) - - -@pytest.mark.asyncio -async def test_engine_rejects_a_bound_embedding_space_change(tmp_path: Path) -> None: - """在变化后的 driver/config 向量进入 Akasha 前拒绝它。""" - - _create_sessions(tmp_path / "sessions.db") - described = _embedding_space() - embeddings = _Embeddings(described) - embeddings.bound = _BoundEmbedding( - replace(described, connection_fingerprint="changed-endpoint") - ) - engine = AkashaMemoryEngine( - embeddings=embeddings, - embedding_space=described, - runtime_scope=_runtime_scope, - akasha_config=AkashaConfig(), - workspace=tmp_path, - event_publisher=None, - ) - try: - with pytest.raises(RuntimeError, match="空间已变化"): - await engine.query( - _query( - "alpha", - datetime(2026, 7, 6, tzinfo=timezone.utc), - intent="context", - ) - ) - finally: - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_feedback_tools_compose_correction_from_two_markers( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Compose correction from forget plus remember without a third action.""" - - # 1. Build one historical turn addressable by either Message ID. - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="old wrong answer", - assistant="incorrect detail", - started=started, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="old wrong answer", - assistant="incorrect detail", - started=started, - ) - ) - await engine._wait_for_publication() # noqa: SLF001 - - # 2. Forget the old Message and remember the current user correction. - specs = {spec.name: spec for spec in engine.tool_profile().tools} - assert set(specs) == {"remember_memory", "forget_memory"} - forget_spec = specs["forget_memory"] - remember_spec = specs["remember_memory"] - assert forget_spec.risk == "write" - assert remember_spec.risk == "write" - assert forget_spec.tool_class is not None - assert remember_spec.tool_class is not None - forget = forget_spec.tool_class(engine, forget_spec) - remember = remember_spec.tool_class(engine, remember_spec) - token = running_turn_id.set("turn:feedback") - try: - forgotten = json.loads( - await forget.execute( - message_ids=["message:1"], - reason="old assistant answer is wrong", - ) - ) - reinforced = json.loads( - await remember.execute( - message_ids=["current_user_message"], - reason="the current user message supplies the correction", - ) - ) - assert forgotten == { - "status": "staged", - "action": "forget", - "target_message_ids": ["message:1"], - "target_turn_ids": ["message:0::message:1"], - "applies_after": "current_turn_commit", - } - assert reinforced == { - "status": "staged", - "action": "remember", - "target_message_ids": ["current_user_message"], - "target_turn_ids": ["current_turn"], - "applies_after": "current_turn_commit", - } - - # 3. Export both independent markers through the narrow Memory port. - metadata = engine.take_turn_user_metadata("turn:feedback") - forgotten_marker = cast(dict[str, object], metadata["akasha_forget"]) - remembered_marker = cast(dict[str, object], metadata["akasha_reinforce"]) - assert forgotten_marker["action"] == "forget" - assert forgotten_marker["target_message_ids"] == ["message:1"] - assert forgotten_marker["target_turn_ids"] == ["message:0::message:1"] - assert remembered_marker["action"] == "remember" - assert remembered_marker["target_message_ids"] == ["current_user_message"] - assert remembered_marker["target_turn_ids"] == ["current_turn"] - assert engine.take_staged_feedback("turn:feedback") == () - finally: - running_turn_id.reset(token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_uncommitted_feedback_does_not_survive_engine_restart( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - spec = next( - item for item in engine.tool_profile().tools if item.name == "remember_memory" - ) - assert spec.tool_class is not None - tool = spec.tool_class(engine, spec) - token = running_turn_id.set("turn:crashed") - try: - result = json.loads( - await tool.execute( - message_ids=["current_user_message"], - reason="staged but not committed", - ) - ) - finally: - running_turn_id.reset(token) - assert result["status"] == "staged" - _close_engine(engine) - - restarted = _engine(tmp_path) - try: - assert restarted.take_turn_user_metadata("turn:crashed") == {} - finally: - _close_engine(restarted) - - -@pytest.mark.asyncio -async def test_feedback_tool_rejects_memory_item_ids( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Require Message identities even when the turn ID looks plausible.""" - - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - try: - spec = next( - spec for spec in engine.tool_profile().tools if spec.name == "forget_memory" - ) - assert spec.tool_class is not None - tool = spec.tool_class(engine, spec) - token = running_turn_id.set("turn:feedback") - try: - result = json.loads( - await tool.execute( - message_ids=["message:0::message:1"], - ) - ) - finally: - running_turn_id.reset(token) - assert result["status"] == "not_staged" - assert result["error"] == "messages_not_in_akasha" - - token = running_turn_id.set("turn:feedback") - try: - current = json.loads( - await tool.execute( - message_ids=["current_user_message"], - ) - ) - finally: - running_turn_id.reset(token) - assert current["status"] == "not_staged" - assert current["error"] == "cannot_forget_current_user_message" - finally: - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_feedback_markers_change_future_recall_and_replay_identically( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Persist tool markers, suppress future recall, and reproduce the graph.""" - - # 1. Commit one wrong historical turn, then stage a current correction. - sessions = tmp_path / "sessions.db" - _create_sessions(sessions) - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - sessions, - sequence=0, - user="alpha old wrong claim", - assistant="wrong answer", - started=started, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha old wrong claim", - assistant="wrong answer", - started=started, - ) - ) - await engine._wait_for_publication() # noqa: SLF001 - specs = {spec.name: spec for spec in engine.tool_profile().tools} - forget_spec = specs["forget_memory"] - remember_spec = specs["remember_memory"] - assert forget_spec.tool_class is not None - assert remember_spec.tool_class is not None - forget = forget_spec.tool_class(engine, forget_spec) - remember = remember_spec.tool_class(engine, remember_spec) - token = running_turn_id.set("turn:correction") - try: - assert ( - json.loads(await forget.execute(message_ids=["message:1"]))["status"] - == "staged" - ) - assert ( - json.loads(await remember.execute(message_ids=["current_user_message"]))[ - "status" - ] - == "staged" - ) - user_extra = engine.take_turn_user_metadata("turn:correction") - finally: - running_turn_id.reset(token) - - # 2. Persist those marker fields on the next canonical user Message. - correction_time = started + timedelta(minutes=5) - _append_turn( - sessions, - sequence=2, - user="beta corrected claim", - assistant="correction accepted", - started=correction_time, - user_extra=user_extra, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=2, - user="beta corrected claim", - assistant="correction accepted", - started=correction_time, - ) - ) - await engine._wait_for_publication() # noqa: SLF001 - - assert engine._runtime.cycle.inhibited_nodes == {0} # noqa: SLF001 - correction = engine._runtime.cycle.turns[1] # noqa: SLF001 - assert correction.feedback.forget_nodes == (0,) - assert correction.feedback.remember_nodes == (1,) - with closing(sqlite3.connect(tmp_path / "memory" / "akasha.db")) as connection: - assert ( - connection.execute(""" - SELECT event_id, action, target_turn_node_id, boost - FROM feedback_events - ORDER BY event_id, action, target_turn_node_id - """).fetchall() - == [ - (1, "forget", 0, 1.0), - (1, "remember", 1, 3.0), - ] - ) - - # 3. Future direct-dense and graph completion lanes hide the old turn. - recalled = await engine.query( - _query( - "alpha old wrong claim", - correction_time + timedelta(minutes=5), - intent="answer", - ) - ) - assert all(record.id != "message:0::message:1" for record in recalled.records) - - # 4. A clean replay consumes the marker and hashes identically. - replay = tmp_path / "memory" / "feedback-replay.db" - rebuild_memory( - tmp_path / "memory" / "akasha-v2-index.db", - replay, - target_sequences=(), - ) - assert logical_state_sha256( - tmp_path / "memory" / "akasha.db" - ) == logical_state_sha256(replay) - _close_engine(engine) - - -def test_mobile_recall_card_projection_preserves_bounded_lanes() -> None: - lane = _mobile_recall_lane( - [ - { - "user_text": "🌙" * 1_000, - "assistant_preview": "🌙" * 1_000, - "assistant_text": "不应进入移动卡片", - "ts": "2026-07-28T00:00:00Z", - "score": 0.5, - } - for _ in range(40) - ] - ) - card = { - "schema": "akasha.recall-card.v1", - "query_id": "query", - "recall_capture_available": True, - "left": lane, - "right": lane, - "tool_left": lane, - "tool_right": lane, - } - - assert len(lane) == 40 - assert all(len(str(item["user_preview"])) == 103 for item in lane) - assert all(len(str(item["assistant_preview"])) == 53 for item in lane) - assert "assistant_text" not in json.dumps(card, ensure_ascii=False) - assert ( - len( - json.dumps( - card, - ensure_ascii=False, - separators=(",", ":"), - ).encode("utf-8") - ) - < 192 * 1024 - ) - - -def test_mobile_inspector_detail_projects_large_assistant_text_to_bounded_rpc() -> None: - large_answer = "答" * (256 * 1024) - lane_item = { - "query_id": "prior-query", - "session_key": "test:one", - "user_text": "问" * 1_000, - "assistant_text": large_answer, - "assistant_preview": large_answer, - "ts": "2026-07-28T00:00:00Z", - "score": 0.5, - "sources": ["dense"], - } - detail = { - "query_id": "query", - "query_text": "当前问题", - "query_preview": "当前问题", - "ts": "2026-07-28T00:00:00Z", - "seed_count": 1, - "activation_capture_available": True, - "recall_capture_available": True, - "activation_count": 1, - "left_count": 1, - "right_count": 1, - "pushes": 1, - "residual_l1": 0.25, - "tool_left_count": 1, - "tool_right_count": 1, - "left": [lane_item], - "right": [lane_item], - "tool_left": [lane_item], - "tool_right": [lane_item], - } - - projected = mobile_summary(detail) - rpc = _normalize_rpc_result( - projected, - plugin_id="akasha", - method="inspector.detail", - ) - encoded = json.dumps(rpc, ensure_ascii=False, separators=(",", ":")) - - assert "assistant_text" not in encoded - assert "user_text" not in encoded - assert len(encoded.encode("utf-8")) < 192 * 1024 - for lane_name in ("left", "right", "tool_left", "tool_right"): - lane = cast(list[dict[str, object]], rpc[lane_name]) - assert set(lane[0]) == { - "user_preview", - "assistant_preview", - "ts", - "score", - } - assert len(str(lane[0]["user_preview"])) == 103 - assert len(str(lane[0]["assistant_preview"])) == 53 - assert lane_item["assistant_text"] == large_answer - - -def test_active_mobile_recall_marks_temporary_absence_as_pending() -> None: - assert _empty_mobile_recall()["pending"] is False - assert _empty_mobile_recall(pending=True)["pending"] is True - - -@pytest.mark.asyncio -async def test_failed_publication_retires_active_recall_and_fails_loud( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Do not present a failed sidecar publication as a valid active handoff.""" - - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - turn_id = "turn:publish-failure" - await engine.query( - MemoryQuery( - text="alpha", - intent="context", - scope=MemoryScope( - session_key="test:one", - channel="test", - chat_id="one", - ), - context={"history": [], "turn_id": turn_id}, - timestamp=started, - ) - ) - other_turn_id = "turn:other-session" - await engine.query( - MemoryQuery( - text="beta", - intent="context", - scope=MemoryScope( - session_key="test:two", - channel="test", - chat_id="two", - ), - context={"history": [], "turn_id": other_turn_id}, - timestamp=started, - ) - ) - assert set(engine._pending) == {"test:one", "test:two"} # noqa: SLF001 - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha", - assistant="answer", - started=started, - ) - - def fail_publish(_staged: object) -> None: - raise RuntimeError("publish boom") - - monkeypatch.setattr(engine, "_publish_staged", fail_publish) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha", - assistant="answer", - started=started, - ) - ) - with pytest.raises(RuntimeError, match="publish boom"): - await engine._wait_for_publication() # noqa: SLF001 - assert "test:one" not in engine._pending # noqa: SLF001 - with pytest.raises(RuntimeError, match="recall publication failed: publish boom"): - engine.wait_for_active_recall("test:one", turn_id, timeout=0) - with pytest.raises(RuntimeError, match="recall publication failed: publish boom"): - engine.wait_for_active_recall("test:two", other_turn_id, timeout=0) - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -@pytest.mark.asyncio -async def test_failed_staging_retires_every_active_recall_and_rpc_fails_loud( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Retire every handoff when durable staging fails before publication.""" - - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - turn_id = "turn:stage-failure" - other_turn_id = "turn:other-session" - for session_key, chat_id, text, active_turn_id in ( - ("test:one", "one", "alpha", turn_id), - ("test:two", "two", "beta", other_turn_id), - ): - await engine.query( - MemoryQuery( - text=text, - intent="context", - scope=MemoryScope( - session_key=session_key, - channel="test", - chat_id=chat_id, - ), - context={"history": [], "turn_id": active_turn_id}, - timestamp=started, - ) - ) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha", - assistant="answer", - started=started, - ) - - def fail_stage(**_kwargs: object) -> None: - raise RuntimeError("stage exploded") - - monkeypatch.setattr(engine._runtime, "stage_from_source", fail_stage) # noqa: SLF001 - with pytest.raises(RuntimeError, match="stage exploded"): - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha", - assistant="answer", - started=started, - ) - ) - - assert engine._pending == {} # noqa: SLF001 - for session_key, active_turn_id in ( - ("test:one", turn_id), - ("test:two", other_turn_id), - ): - with pytest.raises( - RuntimeError, - match="recall publication failed: stage exploded", - ): - engine.wait_for_active_recall(session_key, active_turn_id, timeout=0) - - mobile_query = _AkashaMobileQuery( - _runtime_handle(engine), - memory_root=tmp_path / "memory", - data_root=builtin_plugin_data_dir("akasha", tmp_path), - ) - with pytest.raises( - RuntimeError, - match="recall publication failed: stage exploded", - ): - mobile_query( - "recall.current", - {"message_id": "message:1"}, - session_id="test:one", - turn_id=turn_id, - ) - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -@pytest.mark.asyncio -async def test_cancelled_publication_clears_every_active_recall( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Treat cancellation of the global publication fence as a global failure.""" - - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started_at = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - turn_id = "turn:cancelled-publication" - await engine.query( - MemoryQuery( - text="alpha", - intent="context", - scope=MemoryScope( - session_key="test:one", - channel="test", - chat_id="one", - ), - context={"history": [], "turn_id": turn_id}, - timestamp=started_at, - ) - ) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha", - assistant="answer", - started=started_at, - ) - publish_started = threading.Event() - release_publish = threading.Event() - publish_finished = threading.Event() - - def blocked_publish(_staged: object) -> None: - publish_started.set() - try: - assert release_publish.wait(1.0) - finally: - publish_finished.set() - - monkeypatch.setattr(engine, "_publish_staged", blocked_publish) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha", - assistant="answer", - started=started_at, - ) - ) - assert await asyncio.to_thread(publish_started.wait, 1.0) - publish_task = engine._publish_task # noqa: SLF001 - assert publish_task is not None - publish_task.cancel() - with pytest.raises(asyncio.CancelledError): - await engine._wait_for_publication() # noqa: SLF001 - assert engine._pending == {} # noqa: SLF001 - with pytest.raises(RuntimeError, match="recall publication failed: CancelledError"): - engine.wait_for_active_recall("test:one", turn_id, timeout=0) - release_publish.set() - assert await asyncio.to_thread(publish_finished.wait, 1.0) - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -def test_suffix_loader_and_appendable_features_match_full_replay( - tmp_path: Path, -) -> None: - """Keep the incremental online view identical to full replay features.""" - - # 1. Build one causal source with dense, lexical, and temporal evidence. - sessions = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_sessions(sessions) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - for offset, (user, assistant) in enumerate( - ( - ("alpha first", "first answer"), - ("beta second", "alpha bridge"), - ("alpha third", "final answer"), - ) - ): - _append_turn( - sessions, - sequence=offset * 2, - user=user, - assistant=assistant, - started=started + timedelta(minutes=offset * 3), - with_embeddings=True, - ) - build_sparse_index( - sessions, - index, - BuildConfig( - embedding_model="embedding-model", - embedding_dimension=2, - ), - ) - - # 2. A suffix retains global node IDs, gaps, feedback, and feature bytes. - full = load_turns(index) - suffix = load_turn_suffix(index, 1) - assert len(full) == 3 - assert len(suffix) == 2 - for expected, actual in zip(full[1:], suffix, strict=True): - assert actual.node_id == expected.node_id - assert actual.turn_id == expected.turn_id - assert actual.inter_gap_seconds == expected.inter_gap_seconds - assert actual.user_terms == expected.user_terms - assert actual.assistant_terms == expected.assistant_terms - assert actual.feedback == expected.feedback - assert actual.user_dense is not None - assert expected.user_dense is not None - assert actual.assistant_dense is not None - assert expected.assistant_dense is not None - assert np.array_equal(actual.user_dense, expected.user_dense) - assert np.array_equal( - actual.assistant_dense, - expected.assistant_dense, - ) - - # 3. The O(1) query view and incremental append preserve full-pool results. - online = BurstAwareFeaturePool(full[:2], appendable=True) - replay = BurstAwareFeaturePool(full) - context = online.build_context(((1, 1.0),)) - view = online.query_view(full[2]) - online_decision = view.infer_burst_seed(2, context, (1,), True) - replay_decision = replay.infer_burst_seed(2, context, (1,), True) - assert view.turns is online.turns - assert online_decision.evidence == replay_decision.evidence - assert online_decision.base_continuation == (replay_decision.base_continuation) - assert online_decision.context_dependence == (replay_decision.context_dependence) - assert online_decision.context_mass == replay_decision.context_mass - assert online_decision.continued == replay_decision.continued - for name in online_decision.fields: - assert np.array_equal( - online_decision.fields[name], - replay_decision.fields[name], - ) - online.append_turn(full[2]) - assert np.array_equal(online.turn_dense[:3], replay.turn_dense) - assert np.array_equal(online.lengths[:3], replay.lengths) - assert np.array_equal( - online.context_dependence[:3], - replay.context_dependence, - ) - - # 4. A history without vectors can accept the first later dense turn. - sparse_first = replace( - full[0], - user_dense=None, - assistant_dense=None, - ) - dense_online = BurstAwareFeaturePool( - [sparse_first], - appendable=True, - ) - dense_online.append_turn(full[1]) - dense_replay = BurstAwareFeaturePool([sparse_first, full[1]]) - assert np.array_equal(dense_online.user_dense[:2], dense_replay.user_dense) - assert np.array_equal( - dense_online.assistant_dense[:2], - dense_replay.assistant_dense, - ) - assert np.array_equal(dense_online.turn_dense[:2], dense_replay.turn_dense) - - # 5. Diagnostic path capture cannot change committed online/replay state. - online_cycle = MemoryCycle( - MemoryConfig(), - turn_capacity=len(full), - feature_pool=BurstAwareFeaturePool(full), - ) - replay_cycle = MemoryCycle( - MemoryConfig(), - turn_capacity=len(full), - feature_pool=BurstAwareFeaturePool(full), - ) - for turn in full: - online_cycle.commit( - turn, - online_cycle.retrieve(turn, capture_paths=True), - ) - replay_cycle.commit( - turn, - replay_cycle.retrieve(turn, capture_paths=False), - ) - assert online_cycle.evidence == replay_cycle.evidence - - -@pytest.mark.asyncio -async def test_online_turn_recall_and_replay_share_one_state( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Prove one real host turn grows online and rebuilds identically.""" - - # 1. Start the V2 host adapter on an isolated canonical source. - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - first_query = await engine.query(_query("alpha start", started, intent="context")) - assert first_query.trace["effect"] == "stateful" - assert first_query.records == [] - - # 2. Persist the exact host messages, then commit their retrieval ticket. - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="A" * 80, - started=started, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha start", - assistant="A" * 80, - started=started, - ) - ) - await engine._wait_for_publication() # noqa: SLF001 - assert engine._runtime.cycle.state_version == 1 # noqa: SLF001 - - # 3. Explicit recall is read-only and cannot replace context learning. - next_time = started + timedelta(minutes=5) - active_turn_id = "turn:alpha-follow" - mobile_query = _AkashaMobileQuery( - _runtime_handle(engine), - memory_root=tmp_path / "memory", - data_root=builtin_plugin_data_dir("akasha", tmp_path), - ) - wait_started = threading.Event() - wait_for_active_recall = engine.wait_for_active_recall - - def marked_wait_for_active_recall( - session_key: str, - turn_id: str, - *, - timeout: float = 15.0, - ) -> ActiveRecallSnapshot | None: - wait_started.set() - return wait_for_active_recall( - session_key, - turn_id, - timeout=timeout, - ) - - monkeypatch.setattr( - engine, - "wait_for_active_recall", - marked_wait_for_active_recall, - ) - active_query = asyncio.create_task( - asyncio.to_thread( - mobile_query, - "recall.current", - {"message_id": f"assistant:{active_turn_id}"}, - session_id="test:one", - turn_id=active_turn_id, - ) - ) - assert await asyncio.to_thread(wait_started.wait, 1.0) - context = await engine.query( - MemoryQuery( - text="alpha follow", - intent="context", - scope=MemoryScope( - session_key="test:one", - channel="test", - chat_id="one", - ), - context={"history": [], "turn_id": active_turn_id}, - timestamp=next_time, - ) - ) - active_mobile = await active_query - pending = engine._pending["test:one"] # noqa: SLF001 - tool = RecallMemoryTool( - engine, - cast(Any, engine.tool_profile().recall), - ) - before_recall = logical_state_sha256(tmp_path / "memory" / "akasha.db") - with tool_execution_context_scope( - ToolExecutionContext( - origin_channel="test", - origin_chat_id="one", - origin_session_key="test:one", - current_timestamp=next_time.isoformat(), - ) - ): - rendered = json.loads( - await tool.execute( - query="alpha details", - limit=5, - ) - ) - after_recall = logical_state_sha256(tmp_path / "memory" / "akasha.db") - assert rendered["count"] == 1 - assert before_recall == after_recall - assert engine._pending["test:one"] is pending # noqa: SLF001 - assert context.text_block.startswith("# Akasha memory now=07-06") - assert "## 左脑记忆:精确回忆" in context.text_block - assert f'assistant="{"A" * 50}..."' in context.text_block - assert ( - engine.wait_for_active_recall( - "test:one", - "turn:other", - timeout=0, - ) - is None - ) - - assert [ - item["user_preview"] - for item in cast( - list[dict[str, object]], - active_mobile["left"], - ) - ] == ["alpha start"] - tool_payload = dict(rendered) - tool_payload["items"] = [ - *cast(list[dict[str, object]], rendered["items"]), - { - "id": "tool:completion", - "score": 0.25, - "source_ref": "test:one", - "signals": { - "lane": "completion", - "sources": ["basin_completion"], - "started_at": started.isoformat(), - "user_text": "associated memory", - "assistant_preview": "associated answer", - }, - }, - ] - tool_chain = json.dumps( - [ - { - "calls": [ - { - "name": "recall_memory", - "status": "success", - "result": json.dumps(tool_payload), - } - ] - } - ] - ) - - # 4. Keep the exact prompt lanes readable until both sidecars are published. - publish_entered = threading.Event() - release_publish = threading.Event() - publish_staged = engine._publish_staged # noqa: SLF001 - - def blocked_publish(staged: object) -> None: - publish_entered.set() - assert release_publish.wait(1.0) - publish_staged(cast(Any, staged)) - - monkeypatch.setattr(engine, "_publish_staged", blocked_publish) - _append_turn( - tmp_path / "sessions.db", - sequence=2, - user="alpha follow", - assistant="second answer", - started=next_time, - assistant_tool_chain=tool_chain, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=2, - user="alpha follow", - assistant="second answer", - started=next_time, - ) - ) - assert await asyncio.to_thread(publish_entered.wait, 1.0) - assert engine._pending["test:one"] is pending # noqa: SLF001 - during_publish = mobile_query( - "recall.current", - {"message_id": "message:3"}, - session_id="test:one", - turn_id=active_turn_id, - ) - assert during_publish["left"] == active_mobile["left"] - assert during_publish["right"] == active_mobile["right"] - assert during_publish["pending"] is True - release_publish.set() - await engine._wait_for_publication() # noqa: SLF001 - assert "test:one" not in engine._pending # noqa: SLF001 - published_mobile = mobile_query( - "recall.current", - {"message_id": "message:3"}, - session_id="test:one", - turn_id=active_turn_id, - ) - assert published_mobile.get("pending") is not True - assert len(cast(list[object], published_mobile["tool_left"])) == 1 - assert len(cast(list[object], published_mobile["tool_right"])) == 1 - - # 5. Compare the fully published online learned state with replay. - replay = tmp_path / "memory" / "replay.db" - rebuild_memory( - tmp_path / "memory" / "akasha-v2-index.db", - replay, - target_sequences=(), - ) - assert logical_state_sha256( - tmp_path / "memory" / "akasha.db" - ) == logical_state_sha256(replay) - with closing(sqlite3.connect(tmp_path / "memory" / "akasha.db")) as connection: - assert connection.execute("SELECT COUNT(*) FROM recall_runs").fetchone() == (2,) - assert connection.execute( - "SELECT COUNT(*) FROM activation_runs" - ).fetchone() == (0,) - - # 6. Inspector reconstructs the exact prior-only lanes without writes. - _write_inspector_config(tmp_path) - before_memory = logical_state_sha256(tmp_path / "memory" / "akasha.db") - reader = AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=load_akasha_config( - builtin_plugin_data_dir("akasha", tmp_path) / "config.local.toml" - ), - ) - overview = reader.get_overview() - rows, total = reader.list_turns(q="alpha follow") - detail = reader.get_turn(str(rows[0]["query_id"])) - assert overview["total"] == 2 - assert total == 1 - assert detail is not None - assert detail["query_text"] == "alpha follow" - assert detail["assistant_text"] == "second answer" - assert detail["recall_capture_available"] is True - assert detail["projection_ready"] is True - assert detail["left_count"] == 1 - assert detail["tool_left_count"] == 1 - assert detail["tool_right_count"] == 1 - assert ( - cast(list[dict[str, object]], detail["left"])[0]["user_text"] == "alpha start" - ) - assert "## 左脑记忆:精确回忆" in str(detail["text_block_preview"]) - assert detail["activation_capture_available"] is False - assert before_memory == logical_state_sha256(tmp_path / "memory" / "akasha.db") - - # 7. The desktop API exposes the same state through read-only routes. - app = FastAPI() - register_dashboard( - app, - DashboardContext( - plugin_id="akasha", - plugin_dir=Path("plugins/akasha"), - data_root=builtin_plugin_data_dir("akasha", tmp_path), - validation=False, - _workspace_roots=(("memory", tmp_path / "memory"),), - ), - ) - with TestClient(app) as client: - response = client.get( - "/api/dashboard/akasha-inspector/turns", - params={"q": "alpha follow"}, - ) - assert response.status_code == 200 - assert response.json()["total"] == 1 - api_detail = client.get( - f"/api/dashboard/akasha-inspector/turns/{rows[0]['query_id']}" - ) - assert api_detail.status_code == 200 - assert api_detail.json()["left_count"] == 1 - - # 8. Mobile projections resolve the same committed assistant message. - mobile = mobile_query( - "recall.current", - {"message_id": "message:3"}, - session_id="test:one", - turn_id=None, - ) - recent = mobile_query( - "inspector.recent", - {}, - session_id=None, - turn_id=None, - ) - mobile_detail = mobile_query( - "inspector.detail", - {"query_id": str(rows[0]["query_id"])}, - session_id=None, - turn_id=None, - ) - assert len(cast(list[object], mobile["left"])) == 1 - assert len(cast(list[object], mobile["tool_left"])) == 1 - assert len(cast(list[object], mobile["tool_right"])) == 1 - assert mobile["schema"] == "akasha.recall-card.v1" - mobile_left = cast(list[dict[str, object]], mobile["left"]) - assert mobile_left[0]["user_preview"] == "alpha start" - assert active_mobile["left"] == mobile["left"] - assert active_mobile["right"] == mobile["right"] - assert "user_text" not in mobile_left[0] - assert "assistant_text" not in json.dumps(mobile, ensure_ascii=False) - assert ( - len( - json.dumps( - mobile, - ensure_ascii=False, - separators=(",", ":"), - ).encode("utf-8") - ) - < 16 * 1024 - ) - assert recent["total"] == 2 - assert mobile_detail["query_text"] == "alpha follow" - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_returns_before_graph_publish_and_fences_query( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Release the host turn after durable staging while fencing the next read.""" - - # 1. Block only graph publication after the source and embedding are durable. - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - entered = threading.Event() - release = threading.Event() - publish = engine._runtime.publish_staged # noqa: SLF001 - - def blocked_publish(staged: object) -> object: - entered.set() - if not release.wait(timeout=5): - raise TimeoutError("test graph publication was not released") - return publish(cast(Any, staged)) - - monkeypatch.setattr( - engine._runtime, # noqa: SLF001 - "publish_staged", - blocked_publish, - ) - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - ) - assert await asyncio.to_thread(entered.wait, 1) - assert engine._runtime.cycle.state_version == 0 # noqa: SLF001 - with closing( - sqlite3.connect(tmp_path / "memory" / "akasha-v2-index.db") - ) as connection: - assert connection.execute("SELECT COUNT(*) FROM sparse_turns").fetchone() == ( - 1, - ) - - # 2. The next query waits until the staged graph becomes visible. - query = asyncio.create_task( - engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - ) - await asyncio.sleep(0) - assert not query.done() - release.set() - result = await query - - assert engine._runtime.cycle.state_version == 1 # noqa: SLF001 - assert result.trace["state_version"] == 1 - _close_engine(engine) - - -def _milestone_events(caplog: pytest.LogCaptureFixture) -> list[str]: - return [ - cast(str, record.akashic_fields["event"]) - for record in caplog.records - if getattr(record, "akashic_fields", None) - and record.akashic_fields.get("flow") == "mobile_turn" - ] - - -def _milestone_records( - caplog: pytest.LogCaptureFixture, - *events: str, -) -> list[Any]: - return [ - record - for record in caplog.records - if getattr(record, "akashic_fields", None) - and record.akashic_fields.get("flow") == "mobile_turn" - and record.akashic_fields.get("event") in events - ] - - -def _counts_map(counts: str) -> dict[str, str]: - """Parse the akasha counts payload; operation/span_id are stable keys.""" - - return dict(item.split("=", 1) for item in counts.split(",") if "=" in item) - - -def _milestone_triples( - caplog: pytest.LogCaptureFixture, -) -> list[tuple[str, str, str]]: - """Extract (span_id, operation, event) from every akasha milestone.""" - - triples: list[tuple[str, str, str]] = [] - for record in caplog.records: - fields = cast( - Mapping[str, object] | None, - getattr(record, "akashic_fields", None), - ) - if not fields or fields.get("flow") != "mobile_turn": - continue - counts = _counts_map(str(fields.get("counts") or "")) - triples.append( - ( - counts.get("span_id", ""), - counts.get("operation", ""), - str(fields.get("event")), - ) - ) - return triples - - -def _milestone_span( - caplog: pytest.LogCaptureFixture, - event: str, -) -> str: - """Return the span_id carried by the first occurrence of an event.""" - - records = _milestone_records(caplog, event) - assert records, f"缺少里程碑: {event}" - return _counts_map(str(records[0].akashic_fields["counts"]))["span_id"] - - -def _event_base(event: str) -> str: - for suffix in (".start", ".done", ".error", ".cancelled", ".skip"): - if event.endswith(suffix): - return event[: -len(suffix)] - return event - - -def _assert_span_closed( - caplog: pytest.LogCaptureFixture, - span_id: str, - operation: str, -) -> None: - """每个 (span, event base) 的 start 恰好一个终态;skip 也携同一 span。""" - - by_base: dict[str, list[str]] = {} - for current_span, current_operation, event in _milestone_triples(caplog): - if current_span != span_id: - continue - assert current_operation == operation - base_events = by_base.setdefault(_event_base(event), []) - base_events.append(event) - assert by_base, f"span {span_id} 没有任何里程碑记录" - for base, events in by_base.items(): - start = f"{base}.start" - if start in events: - assert events.count(start) == 1 - assert len([e for e in events if e != start]) == 1 - else: - assert events == [f"{base}.skip"] or events == ["akasha.publish_scheduled"] - - -@pytest.mark.asyncio -async def test_turn_commit_blocked_embed_keeps_fanout_open_at_embed_start( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """TurnCommitted fanout 在 embed 被阻塞时未完成,阶段停在 embed.start。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - entered = asyncio.Event() - release = asyncio.Event() - original_embed_batch = engine._embedder.embed_batch # noqa: SLF001 - - async def blocked_embed_batch(texts: list[str]) -> list[list[float]]: - entered.set() - await release.wait() - return await original_embed_batch(texts) - - monkeypatch.setattr( - engine._embedder, # noqa: SLF001 - "embed_batch", - blocked_embed_batch, - ) - # contextvar 身份与事件不同,证明 commit 路径身份显式来自事件。 - session_token = current_session_key.set("ctx:session") - client_token = current_client_message_id.set("ctx-client") - turn_token = running_turn_id.set("ctx-turn") - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - commit_task: asyncio.Task[None] | None = None - try: - commit_task = asyncio.create_task( - engine._on_turn_committed(event) # noqa: SLF001 - ) - assert await asyncio.wait_for(entered.wait(), timeout=1) - assert not commit_task.done() - assert _milestone_events(caplog) == [ - "akasha.turn_commit.start", - "akasha.source_gate.wait.start", - "akasha.source_gate.wait.done", - "akasha.embed.start", - ] - assert "akasha.embed.done" not in _milestone_events(caplog) - assert "akasha.commit_gate.wait.done" not in _milestone_events(caplog) - assert "akasha.stage.done" not in _milestone_events(caplog) - assert "akasha.turn_commit.done" not in _milestone_events(caplog) - embed_records = _milestone_records(caplog, "akasha.embed.start") - embed_counts = _counts_map(str(embed_records[0].akashic_fields["counts"])) - assert embed_counts["embed_mode"] == "batch" - assert embed_counts["operation"] == "turn_commit" - assert embed_counts["span_id"] == _milestone_span( - caplog, "akasha.turn_commit.start" - ) - for record in embed_records: - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - finally: - release.set() - if commit_task is not None: - await commit_task - await engine._wait_for_publication() # noqa: SLF001 - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_release_orders_stage_before_turn_commit_done( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """解除 embed 阻塞后,阶段顺序完整,stage.done 先于 turn_commit.done。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - entered = asyncio.Event() - release = asyncio.Event() - clock = {"now": 0.0} - monkeypatch.setattr( - "plugins.akasha.engine.perf_counter", - lambda: clock["now"], - ) - original_embed_batch = engine._embedder.embed_batch # noqa: SLF001 - - async def blocked_embed_batch(texts: list[str]) -> list[list[float]]: - entered.set() - await release.wait() - clock["now"] += 0.25 - return await original_embed_batch(texts) - - monkeypatch.setattr( - engine._embedder, # noqa: SLF001 - "embed_batch", - blocked_embed_batch, - ) - session_token = current_session_key.set("ctx:session") - client_token = current_client_message_id.set("ctx-client") - turn_token = running_turn_id.set("ctx-turn") - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - commit_task: asyncio.Task[None] | None = None - try: - commit_task = asyncio.create_task( - engine._on_turn_committed(event) # noqa: SLF001 - ) - assert await asyncio.wait_for(entered.wait(), timeout=1) - release.set() - await commit_task - await engine._wait_for_publication() # noqa: SLF001 - assert _milestone_events(caplog) == [ - "akasha.turn_commit.start", - "akasha.source_gate.wait.start", - "akasha.source_gate.wait.done", - "akasha.embed.start", - "akasha.embed.done", - "akasha.commit_gate.wait.start", - "akasha.commit_gate.wait.done", - "akasha.prior_publication.wait.start", - "akasha.prior_publication.wait.done", - "akasha.stage.start", - "akasha.stage.done", - "akasha.publish_scheduled", - "akasha.turn_commit.done", - ] - assert commit_task.done() - # 所有 commit 里程碑身份来自事件,而非 contextvar,且共享同一 span。 - span_id = _milestone_span(caplog, "akasha.turn_commit.start") - for record in caplog.records: - if not getattr(record, "akashic_fields", None): - continue - if record.akashic_fields.get("flow") != "mobile_turn": - continue - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - counts = _counts_map(str(cast(Any, record.akashic_fields)["counts"])) - assert counts["span_id"] == span_id - assert counts["operation"] == "turn_commit" - _assert_span_closed(caplog, span_id, "turn_commit") - # 可控单调时钟证明 stage.done 不把 embed 阻塞时间算入自身 span。 - embed_done = _milestone_records(caplog, "akasha.embed.done")[0] - stage_done = _milestone_records(caplog, "akasha.stage.done")[0] - assert cast(float, embed_done.akashic_fields["duration_ms"]) == 250.0 - assert cast(float, stage_done.akashic_fields["duration_ms"]) == 0.0 - finally: - release.set() - if commit_task is not None: - await commit_task - await engine._wait_for_publication() # noqa: SLF001 - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_query_blocked_publication_is_locatable_at_wait( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """query 阻塞在 publication wait 时,可通过里程碑定位到等待阶段。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - entered = threading.Event() - release = threading.Event() - publish = engine._runtime.publish_staged # noqa: SLF001 - - def blocked_publish(staged: object) -> object: - entered.set() - if not release.wait(timeout=5): - raise TimeoutError("test graph publication was not released") - return publish(cast(Any, staged)) - - monkeypatch.setattr( - engine._runtime, # noqa: SLF001 - "publish_staged", - blocked_publish, - ) - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - query_task: asyncio.Task[MemoryQueryResult] | None = None - try: - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - ) - assert await asyncio.to_thread(entered.wait, 1) - query_task = asyncio.create_task( - engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - ) - await asyncio.sleep(0) - events = _milestone_events(caplog) - assert "akasha.query.start" in events - assert "akasha.embed.done" in events - # commit 自身已产生一对 prior_publication.wait;query 的那对只到 start。 - assert events.count("akasha.prior_publication.wait.start") == 2 - assert events.count("akasha.prior_publication.wait.done") == 1 - assert "akasha.runtime.query.done" not in events - assert "akasha.query.done" not in events - assert not query_task.done() - release.set() - result = await query_task - assert result.trace["state_version"] == 1 - assert _milestone_events(caplog)[-1] == "akasha.query.done" - assert ( - _milestone_events(caplog).count("akasha.prior_publication.wait.done") == 2 - ) - done_records = _milestone_records(caplog, "akasha.query.done") - assert "hits" in _counts_map(str(done_records[-1].akashic_fields["counts"])) - # query 里程碑身份来自当前 turn contextvar。 - for record in _milestone_records(caplog, "akasha.query.start"): - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "turn-1" - assert record.akashic_fields["client_message_id"] == "client-message-1" - # 同 turn 的 query 与 turn_commit 各持独立 span,且都闭合。 - query_span = _milestone_span(caplog, "akasha.query.start") - commit_span = _milestone_span(caplog, "akasha.turn_commit.start") - assert query_span != commit_span - _assert_span_closed(caplog, query_span, "query") - _assert_span_closed(caplog, commit_span, "turn_commit") - # embed 事件可按 operation 区分归属。 - assert { - operation - for _, operation, event in _milestone_triples(caplog) - if event.startswith("akasha.embed.") - } == {"query", "turn_commit"} - finally: - release.set() - if query_task is not None and not query_task.done(): - await query_task - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_embed_failure_records_embed_error_and_turn_commit_error( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """embed 异常产生 embed.error 与 turn_commit.error,均无 done。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - - async def exploding_embed_batch(texts: list[str]) -> list[list[float]]: - raise RuntimeError("embed exploded") - - monkeypatch.setattr( - engine._embedder, # noqa: SLF001 - "embed_batch", - exploding_embed_batch, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - with pytest.raises(RuntimeError, match="embed exploded"): - await engine._on_turn_committed(event) # noqa: SLF001 - assert _milestone_events(caplog) == [ - "akasha.turn_commit.start", - "akasha.source_gate.wait.start", - "akasha.source_gate.wait.done", - "akasha.embed.start", - "akasha.embed.error", - "akasha.turn_commit.error", - ] - assert "akasha.embed.done" not in _milestone_events(caplog) - assert "akasha.turn_commit.done" not in _milestone_events(caplog) - for event_name in ("akasha.embed.error", "akasha.turn_commit.error"): - for record in _milestone_records(caplog, event_name): - assert record.levelno == logging.ERROR - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.turn_commit.start"), - "turn_commit", - ) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_source_gate_wait_records_blocked_duration( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """source gate 被外部持有时,wait.start 后停在等待,释放后带 duration。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - gate_task = asyncio.create_task(engine._source_event_gate.acquire()) # noqa: SLF001 - await gate_task - commit_task = asyncio.create_task(engine._on_turn_committed(event)) # noqa: SLF001 - try: - await asyncio.sleep(0.1) - assert not commit_task.done() - events = _milestone_events(caplog) - assert "akasha.turn_commit.start" in events - assert "akasha.source_gate.wait.start" in events - assert "akasha.source_gate.wait.done" not in events - assert "akasha.embed.start" not in events - assert "akasha.turn_commit.done" not in events - engine._source_event_gate.release() # noqa: SLF001 - await asyncio.wait_for(commit_task, timeout=5) - await engine._wait_for_publication() # noqa: SLF001 - wait_done = _milestone_records(caplog, "akasha.source_gate.wait.done") - assert wait_done - assert cast(float, wait_done[0].akashic_fields["duration_ms"]) >= 50.0 - assert _milestone_events(caplog)[-1] == "akasha.turn_commit.done" - finally: - if engine._source_event_gate.locked(): # noqa: SLF001 - engine._source_event_gate.release() # noqa: SLF001 - if not commit_task.done(): - await commit_task - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_commit_gate_wait_records_blocked_duration( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """commit gate 被外部持有时,embed 完成后停在 wait,释放后带 duration。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - gate_task = asyncio.create_task(engine._commit_gate.acquire()) # noqa: SLF001 - await gate_task - commit_task = asyncio.create_task(engine._on_turn_committed(event)) # noqa: SLF001 - try: - await asyncio.sleep(0.1) - assert not commit_task.done() - events = _milestone_events(caplog) - assert "akasha.embed.done" in events - assert "akasha.commit_gate.wait.start" in events - assert "akasha.commit_gate.wait.done" not in events - assert "akasha.stage.start" not in events - assert "akasha.turn_commit.done" not in events - engine._commit_gate.release() # noqa: SLF001 - await asyncio.wait_for(commit_task, timeout=5) - await engine._wait_for_publication() # noqa: SLF001 - wait_done = _milestone_records(caplog, "akasha.commit_gate.wait.done") - assert wait_done - assert cast(float, wait_done[0].akashic_fields["duration_ms"]) >= 50.0 - assert _milestone_events(caplog)[-1] == "akasha.turn_commit.done" - finally: - if engine._commit_gate.locked(): # noqa: SLF001 - engine._commit_gate.release() # noqa: SLF001 - if not commit_task.done(): - await commit_task - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_event_bus_fanout_swallows_akasha_error_but_keeps_error_milestones( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """EventBus fanout 吞掉 Akasha handler 异常,但错误里程碑仍可定位。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - bus = EventBus() - engine = _engine(tmp_path, event_publisher=bus) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - - async def exploding_embed_batch(texts: list[str]) -> list[list[float]]: - raise RuntimeError("embed exploded") - - monkeypatch.setattr( - engine._embedder, # noqa: SLF001 - "embed_batch", - exploding_embed_batch, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - await bus.fanout(event) - events = _milestone_events(caplog) - assert "akasha.turn_commit.start" in events - assert "akasha.embed.error" in events - assert "akasha.turn_commit.error" in events - assert "akasha.embed.done" not in events - assert "akasha.turn_commit.done" not in events - for record in _milestone_records(caplog, "akasha.turn_commit.error"): - assert record.levelno == logging.ERROR - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.turn_commit.start"), - "turn_commit", - ) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_query_embed_failure_closes_span_with_error( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """query embed 异常:embed.error 与 query.error 闭合,无 done。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - - async def exploding_embed(text: str) -> list[float]: - raise RuntimeError("query embed exploded") - - monkeypatch.setattr(engine._embedder, "embed", exploding_embed) # noqa: SLF001 - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - try: - with pytest.raises(RuntimeError, match="query embed exploded"): - await engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - assert _milestone_events(caplog) == [ - "akasha.query.start", - "akasha.embed.start", - "akasha.embed.error", - "akasha.query.error", - ] - assert "akasha.query.done" not in _milestone_events(caplog) - for event_name in ("akasha.embed.error", "akasha.query.error"): - for record in _milestone_records(caplog, event_name): - assert record.levelno == logging.ERROR - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "turn-1" - assert record.akashic_fields["client_message_id"] == "client-message-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.query.start"), - "query", - ) - finally: - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_query_cancelled_while_blocked_on_commit_gate_closes_span( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """query 阻塞在 commit gate 时被取消:query.cancelled 闭合,无 done/error。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - gate_task = asyncio.create_task(engine._commit_gate.acquire()) # noqa: SLF001 - await gate_task - query_task = asyncio.create_task( - engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - ) - try: - await asyncio.sleep(0.05) - assert not query_task.done() - events = _milestone_events(caplog) - assert "akasha.query.start" in events - assert "akasha.embed.done" in events - assert "akasha.commit_gate.wait.start" in events - assert "akasha.commit_gate.wait.done" not in events - assert "akasha.query.done" not in events - query_task.cancel() - with pytest.raises(asyncio.CancelledError): - await query_task - events = _milestone_events(caplog) - assert "akasha.query.cancelled" in events - assert "akasha.commit_gate.wait.cancelled" in events - assert "akasha.query.done" not in events - assert "akasha.query.error" not in events - assert "akasha.commit_gate.wait.done" not in events - for record in _milestone_records( - caplog, "akasha.query.cancelled", "akasha.commit_gate.wait.cancelled" - ): - assert record.akashic_fields["outcome"] == "cancelled" - assert record.akashic_fields["duration_ms"] is not None - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "turn-1" - assert record.akashic_fields["client_message_id"] == "client-message-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.query.start"), - "query", - ) - finally: - engine._commit_gate.release() # noqa: SLF001 - if not query_task.done(): - await query_task - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -async def _wait_for_milestone( - caplog: pytest.LogCaptureFixture, - event: str, - *, - timeout_s: float = 5.0, -) -> None: - """轮询等待某个里程碑出现;避免纯 sleep 的不稳定时序。""" - - for _ in range(int(timeout_s * 100)): - if event in _milestone_events(caplog): - return - await asyncio.sleep(0.01) - raise AssertionError(f"milestone 未在 {timeout_s}s 内出现: {event}") - - -@pytest.mark.asyncio -async def test_turn_commit_source_skip_closes_total_span_with_skipped_outcome( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """source 代际失效的 skip 不得绕过 turn_commit.done,须以 skipped 收口。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - with engine._lock: # noqa: SLF001 - engine._source_generation += 1 # noqa: SLF001 - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - gate_task = asyncio.create_task(engine._source_event_gate.acquire()) # noqa: SLF001 - await gate_task - commit_task = asyncio.create_task(engine._on_turn_committed(event)) # noqa: SLF001 - try: - await _wait_for_milestone(caplog, "akasha.source_gate.wait.start") - assert not commit_task.done() - # 等 gate 期间 source 代际推进,handler 恢复后必须走 skip 而非静默 return。 - with engine._lock: # noqa: SLF001 - engine._source_generation += 1 # noqa: SLF001 - engine._source_event_gate.release() # noqa: SLF001 - await asyncio.wait_for(commit_task, timeout=5) - assert _milestone_events(caplog) == [ - "akasha.turn_commit.start", - "akasha.source_gate.wait.start", - "akasha.source_gate.wait.done", - "akasha.source_event.skip", - "akasha.turn_commit.done", - ] - done = _milestone_records(caplog, "akasha.turn_commit.done")[0] - assert done.akashic_fields["outcome"] == "skipped" - assert done.akashic_fields["duration_ms"] is not None - assert "akasha.embed.start" not in _milestone_events(caplog) - assert "akasha.turn_commit.error" not in _milestone_events(caplog) - assert "akasha.turn_commit.cancelled" not in _milestone_events(caplog) - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.turn_commit.start"), - "turn_commit", - ) - finally: - if engine._source_event_gate.locked(): # noqa: SLF001 - engine._source_event_gate.release() # noqa: SLF001 - if not commit_task.done(): - await commit_task - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_stage_failure_after_gate_records_stage_error_not_gate_error( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """commit_gate 取得后 stage 异常:记录 stage.error,不伪装成 gate.error。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - - def exploding_stage(*args: object, **kwargs: object) -> object: - raise RuntimeError("stage exploded") - - monkeypatch.setattr( - engine._runtime, # noqa: SLF001 - "stage_from_source", - exploding_stage, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - try: - with pytest.raises(RuntimeError, match="stage exploded"): - await engine._on_turn_committed(event) # noqa: SLF001 - events = _milestone_events(caplog) - assert "akasha.commit_gate.wait.done" in events - assert "akasha.stage.start" in events - assert "akasha.stage.error" in events - assert "akasha.turn_commit.error" in events - assert "akasha.commit_gate.wait.error" not in events - assert "akasha.stage.done" not in events - assert "akasha.turn_commit.done" not in events - for event_name in ("akasha.stage.error", "akasha.turn_commit.error"): - for record in _milestone_records(caplog, event_name): - assert record.akashic_fields["outcome"] == "error" - assert record.levelno == logging.ERROR - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.turn_commit.start"), - "turn_commit", - ) - finally: - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_turn_commit_cancelled_while_waiting_on_commit_gate_closes_wait_and_total( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """等待 commit gate 时取消:commit_gate.wait.cancelled + turn_commit.cancelled。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - event = _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - gate_task = asyncio.create_task(engine._commit_gate.acquire()) # noqa: SLF001 - await gate_task - commit_task = asyncio.create_task(engine._on_turn_committed(event)) # noqa: SLF001 - try: - await _wait_for_milestone(caplog, "akasha.commit_gate.wait.start") - assert not commit_task.done() - assert "akasha.commit_gate.wait.done" not in _milestone_events(caplog) - commit_task.cancel() - with pytest.raises(asyncio.CancelledError): - await commit_task - events = _milestone_events(caplog) - assert "akasha.commit_gate.wait.cancelled" in events - assert "akasha.turn_commit.cancelled" in events - assert "akasha.commit_gate.wait.done" not in events - assert "akasha.stage.start" not in events - assert "akasha.turn_commit.done" not in events - for event_name in ( - "akasha.commit_gate.wait.cancelled", - "akasha.turn_commit.cancelled", - ): - for record in _milestone_records(caplog, event_name): - assert record.akashic_fields["outcome"] == "cancelled" - assert record.akashic_fields["duration_ms"] is not None - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "event-turn-1" - assert record.akashic_fields["client_message_id"] == "event-client-1" - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.turn_commit.start"), - "turn_commit", - ) - finally: - if not commit_task.done(): - await commit_task - engine._commit_gate.release() # noqa: SLF001 - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_query_runtime_failure_closes_runtime_query_with_error( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """runtime.query 异常:runtime.query.error + query.error,均不伪装 wait 失败。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - - def exploding_query_turn(*args: object, **kwargs: object) -> object: - raise RuntimeError("graph exploded") - - monkeypatch.setattr( - engine._runtime, # noqa: SLF001 - "query_turn", - exploding_query_turn, - ) - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - try: - with pytest.raises(RuntimeError, match="graph exploded"): - await engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - assert _milestone_events(caplog) == [ - "akasha.query.start", - "akasha.embed.start", - "akasha.embed.done", - "akasha.commit_gate.wait.start", - "akasha.commit_gate.wait.done", - "akasha.prior_publication.wait.start", - "akasha.prior_publication.wait.done", - "akasha.runtime.query.start", - "akasha.runtime.query.error", - "akasha.query.error", - ] - for event_name in ("akasha.runtime.query.error", "akasha.query.error"): - for record in _milestone_records(caplog, event_name): - assert record.akashic_fields["outcome"] == "error" - assert record.levelno == logging.ERROR - assert record.akashic_fields["session_id"] == "test:one" - assert record.akashic_fields["turn_id"] == "turn-1" - assert record.akashic_fields["client_message_id"] == "client-message-1" - assert "akasha.runtime.query.done" not in _milestone_events(caplog) - assert "akasha.query.done" not in _milestone_events(caplog) - _assert_span_closed( - caplog, - _milestone_span(caplog, "akasha.query.start"), - "query", - ) - finally: - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -def test_embedding_preflight_excludes_scheduler_but_reports_dialogue_gap( - tmp_path: Path, -) -> None: - """Keep background jobs out while failing on missing dialogue vectors.""" - - # 1. Create one complete scheduler turn and one incomplete user turn. - sessions = tmp_path / "sessions.db" - _create_sessions(sessions) - started = datetime(2026, 7, 6, tzinfo=timezone.utc) - _append_turn( - sessions, - sequence=0, - user="background", - assistant="background answer", - started=started, - session_key="scheduler:job", - with_embeddings=False, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - _append_turn( - sessions, - sequence=0, - user="dialogue", - assistant="dialogue answer", - started=started, - with_embeddings=False, - ) - - # 2. Only the real dialogue gap belongs in the strict migration report. - audit = audit_source_embeddings( - sessions, - BuildConfig( - embedding_model="embedding-model", - embedding_dimension=2, - ), - ) - assert audit.eligible_turns == 1 - assert [issue.session_key for issue in audit.issues] == [ - "test:one", - "test:one", - ] - - -def test_embedding_preflight_excludes_legacy_interrupted_turn( - tmp_path: Path, -) -> None: - """Keep an interrupted placeholder outside the strict replay boundary.""" - - # 1. Persist the historical marker without embeddings or structured flags. - sessions = tmp_path / "sessions.db" - _create_sessions(sessions) - _append_turn( - sessions, - sequence=0, - user="unfinished request", - assistant="[interrupted]", - started=datetime(2026, 7, 6, tzinfo=timezone.utc), - with_embeddings=False, - ) - - # 2. Classify the turn as excluded instead of an embedding defect. - audit = audit_source_embeddings( - sessions, - BuildConfig( - embedding_model="embedding-model", - embedding_dimension=2, - ), - ) - - assert audit.complete - assert audit.eligible_turns == 0 - assert audit.excluded_interrupted_turns == 1 - assert audit.issues == () - - -def test_sparse_builder_ignores_twenty_proactive_messages_before_user_turn( - tmp_path: Path, -) -> None: - """忽略二十条无人回复的 proactive,只学习随后的完整 interaction。""" - - # 1. 构造 proactive 先提交、completed interaction 后提交的 canonical 顺序。 - sessions = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_sessions(sessions) - started = datetime(2026, 8, 6, tzinfo=timezone.utc) - rows = [ - ( - "u1", - 20, - "user", - "alpha", - {"control_turn_id": "t1", "turn_input_ordinal": 0}, - ), - ( - "u2", - 21, - "user", - "beta", - {"control_turn_id": "t1", "turn_input_ordinal": 1}, - ), - ( - "u3", - 22, - "user", - "gamma", - {"control_turn_id": "t1", "turn_input_ordinal": 2}, - ), - ( - "a1", - 23, - "assistant", - "final", - { - "control_turn_id": "t1", - "turn_terminal": True, - "turn_input_count": 3, - }, - ), - ] - vectors = { - "u1": [1.0, 0.0], - "u2": [0.0, 1.0], - "u3": [1.0, 0.0], - "a1": [0.0, 1.0], - } - with closing(sqlite3.connect(sessions)) as connection, connection: - connection.execute( - "INSERT INTO sessions VALUES ('test:one', ?, ?, 0, NULL)", - (started.isoformat(), started.isoformat()), - ) - for sequence in range(20): - connection.execute( - "INSERT INTO messages VALUES (?, 'test:one', ?, ?, ?, NULL, ?, ?)", - ( - f"p{sequence}", - sequence, - "assistant", - f"proactive {sequence}", - json.dumps( - { - "proactive": True, - "delivery_id": f"delivery-{sequence}", - "control_turn_id": f"proactive-turn-{sequence}", - } - ), - (started + timedelta(seconds=sequence)).isoformat(), - ), - ) - for message_id, seq, role, content, extra in rows: - connection.execute( - "INSERT INTO messages VALUES (?, 'test:one', ?, ?, ?, NULL, ?, ?)", - ( - message_id, - seq, - role, - content, - json.dumps(extra), - (started + timedelta(seconds=seq)).isoformat(), - ), - ) - connection.execute( - "INSERT INTO message_embeddings VALUES (?, ?, 'embedding-model', ?, 2, ?, ?)", - ( - message_id, - hashlib.sha256(content.encode()).hexdigest(), - sqlite3.Binary(struct.pack("<2f", *vectors[message_id])), - started.isoformat(), - started.isoformat(), - ), - ) - - # 2. 构建器只把显式 interaction 聚合成学习样本。 - result = build_sparse_index( - sessions, - index, - BuildConfig(embedding_model="embedding-model", embedding_dimension=2), - ) - with closing(sqlite3.connect(index)) as connection: - turn = connection.execute( - "SELECT user_message_id, assistant_message_id, user_text FROM sparse_turns" - ).fetchone() - dense = connection.execute( - "SELECT source_id, embedding FROM turn_dense WHERE field = 'user'" - ).fetchone() - - assert result.indexed_turns == 1 - assert turn == ("u1", "a1", "alpha\n\nbeta\n\ngamma") - assert dense is not None and dense[0] == "u1" - user_dense = struct.unpack("<2f", dense[1]) - assert user_dense == pytest.approx((2 / math.sqrt(5), 1 / math.sqrt(5))) - - -def test_sparse_builder_appends_resumed_turn_by_terminal_commit_time( - tmp_path: Path, -) -> None: - """跨天恢复的 Turn 按最终提交时间追加,不按首个输入时间倒插。""" - - # 1. 先建立已有高水位,再补入一个更早开始但更晚完成的 interaction。 - sessions = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_sessions(sessions) - existing_started = datetime(2026, 8, 15, tzinfo=timezone.utc) - _append_turn( - sessions, - sequence=0, - user="existing", - assistant="existing answer", - started=existing_started, - session_key="test:existing", - with_embeddings=True, - ) - config = BuildConfig( - embedding_model="embedding-model", - embedding_dimension=2, - ) - build_sparse_index(sessions, index, config) - - resumed_started = datetime(2026, 8, 12, tzinfo=timezone.utc) - resumed_at = datetime(2026, 8, 16, tzinfo=timezone.utc) - rows = ( - ( - "resume:u1", - 0, - "user", - "old input", - {"control_turn_id": "turn:resume", "turn_input_ordinal": 0}, - resumed_started, - ), - ( - "resume:u2", - 1, - "user", - "current input", - {"control_turn_id": "turn:resume", "turn_input_ordinal": 1}, - resumed_at, - ), - ( - "resume:a1", - 2, - "assistant", - "final answer", - { - "control_turn_id": "turn:resume", - "turn_terminal": True, - "turn_input_count": 2, - }, - resumed_at + timedelta(seconds=10), - ), - ) - with closing(sqlite3.connect(sessions)) as connection, connection: - connection.execute( - "INSERT INTO sessions VALUES ('test:resume', ?, ?, 0, NULL)", - (resumed_started.isoformat(), resumed_at.isoformat()), - ) - for message_id, seq, role, content, extra, timestamp in rows: - connection.execute( - "INSERT INTO messages VALUES (?, 'test:resume', ?, ?, ?, NULL, ?, ?)", - ( - message_id, - seq, - role, - content, - json.dumps(extra), - timestamp.isoformat(), - ), - ) - connection.execute( - "INSERT INTO message_embeddings VALUES (?, ?, 'embedding-model', ?, 2, ?, ?)", - ( - message_id, - hashlib.sha256(content.encode()).hexdigest(), - sqlite3.Binary(struct.pack("<2f", 1.0, 0.0)), - timestamp.isoformat(), - timestamp.isoformat(), - ), - ) - - # 2. 增量与 replay 都把恢复 Turn 放在最终提交位置。 - result = build_sparse_index(sessions, index, config) - turns = load_turns(index) - - assert result.indexed_turns == 1 - assert [turn.turn_id for turn in turns] == [ - "test:existing:0::test:existing:1", - "resume:u1::resume:a1", - ] - assert turns[1].started_at == resumed_started.isoformat() - assert turns[1].committed_at == (resumed_at + timedelta(seconds=10)).isoformat() - assert turns[1].inter_gap_seconds == pytest.approx(86400.0) - - -def test_sparse_builder_rejects_orphan_message_push_turn(tmp_path: Path) -> None: - """不能把工具使用记录误当成合法的主动 Turn 身份。""" - - # 1. 构造只有 message_push 执行证据、没有 proactive 身份的孤儿 Turn。 - sessions = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_sessions(sessions) - started = datetime(2026, 8, 6, tzinfo=timezone.utc) - with closing(sqlite3.connect(sessions)) as connection, connection: - connection.execute( - "INSERT INTO sessions VALUES ('test:one', ?, ?, 0, NULL)", - (started.isoformat(), started.isoformat()), - ) - connection.execute( - "INSERT INTO messages VALUES (?, 'test:one', 0, ?, ?, NULL, ?, ?)", - ( - "a1", - "assistant", - "orphan outbound", - json.dumps( - { - "control_turn_id": "orphan-turn", - "tools_used": ["message_push"], - } - ), - started.isoformat(), - ), - ) - - # 2. 普通孤儿 Turn 仍由 replay 边界明确拒绝。 - with pytest.raises(ValueError, match="同 turn transcript 结构无效: orphan-turn"): - build_sparse_index( - sessions, - index, - BuildConfig( - embedding_model="embedding-model", - embedding_dimension=2, - ), - ) - - -def _seed_two_explicit_akasha_turns(workspace: Path) -> datetime: - """Persist two canonical explicit turns with frozen embeddings.""" - - sessions = workspace / "sessions.db" - _create_sessions(sessions) - started = datetime(2026, 8, 7, tzinfo=timezone.utc) - rows = [ - ( - "u1", - 0, - "user", - "alpha", - {"control_turn_id": "t1", "turn_input_ordinal": 0}, - ), - ( - "u2", - 1, - "user", - "continue", - {"control_turn_id": "t1", "turn_input_ordinal": 1}, - ), - ( - "a1", - 2, - "assistant", - "first", - { - "control_turn_id": "t1", - "turn_terminal": True, - "turn_input_count": 2, - }, - ), - ( - "u3", - 3, - "user", - "beta", - {"control_turn_id": "t2", "turn_input_ordinal": 0}, - ), - ( - "a2", - 4, - "assistant", - "second", - { - "control_turn_id": "t2", - "turn_terminal": True, - "turn_input_count": 1, - }, - ), - ] - with closing(sqlite3.connect(sessions)) as connection, connection: - connection.execute( - "INSERT INTO sessions VALUES ('test:one', ?, ?, 5, NULL)", - (started.isoformat(), started.isoformat()), - ) - for message_id, seq, role, content, extra in rows: - connection.execute( - "INSERT INTO messages VALUES (?, 'test:one', ?, ?, ?, NULL, ?, ?)", - ( - message_id, - seq, - role, - content, - json.dumps(extra), - (started + timedelta(seconds=seq)).isoformat(), - ), - ) - vector = (1.0, 0.0) if role == "user" else (0.0, 1.0) - connection.execute( - "INSERT INTO message_embeddings VALUES (?, ?, ?, ?, 2, ?, ?)", - ( - message_id, - hashlib.sha256(content.encode()).hexdigest(), - TEST_EMBEDDING_IDENTITY, - sqlite3.Binary(struct.pack("<2f", *vector)), - started.isoformat(), - started.isoformat(), - ), - ) - return started - - -@pytest.mark.asyncio -async def test_interaction_deletion_rebuilds_akasha_and_clears_pending( - tmp_path: Path, -) -> None: - started = _seed_two_explicit_akasha_turns(tmp_path) - sessions = tmp_path / "sessions.db" - - engine = _engine(tmp_path) - first_turn = engine._runtime.cycle.turns[0] # noqa: SLF001 - assert first_turn.user_dense is not None - _, ticket = engine._runtime.query_turn( # noqa: SLF001 - text=first_turn.user_text, - dense=first_turn.user_dense, - session_key="test:one", - timestamp=started + timedelta(seconds=10), - ) - engine._pending["test:one"] = PendingRetrieval( # noqa: SLF001 - ticket=ticket, - query_timestamp=started, - query_text=first_turn.user_text, - query_dense=first_turn.user_dense.copy(), - turn_id="attempt-3", - records=RetrievalRecords(dense=(), completion=()), - ) - engine._pending["test:other"] = engine._pending["test:one"] # noqa: SLF001 - store = SessionStore(sessions) - deletion = await engine.delete_interaction_source( - "t1", - lambda: store.delete_interaction("t1"), - ) - assert deletion is not None - - assert "test:one" not in engine._pending # noqa: SLF001 - assert "test:other" not in engine._pending # noqa: SLF001 - assert [turn.turn_id for turn in engine._runtime.cycle.turns] == [ # noqa: SLF001 - "u3::a2" - ] - with closing( - sqlite3.connect(tmp_path / "memory" / "akasha-v2-index.db") - ) as connection: - assert connection.execute( - "SELECT turn_id FROM sparse_turns ORDER BY turn_id" - ).fetchall() == [("u3::a2",)] - with closing(sqlite3.connect(tmp_path / "memory" / "akasha.db")) as connection: - assert connection.execute( - "SELECT turn_id FROM turn_nodes ORDER BY turn_id" - ).fetchall() == [("u3::a2",)] - store.close() - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -@pytest.mark.asyncio -async def test_interaction_deletion_waits_for_in_flight_source_embedding( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - started = _seed_two_explicit_akasha_turns(tmp_path) - with closing(sqlite3.connect(tmp_path / "sessions.db")) as connection, connection: - connection.execute( - "DELETE FROM message_embeddings WHERE message_id IN ('u3', 'a2')" - ) - connection.execute("DELETE FROM messages WHERE id IN ('u3', 'a2')") - engine = _engine(tmp_path) - with closing(sqlite3.connect(tmp_path / "sessions.db")) as connection, connection: - connection.execute( - "INSERT INTO messages VALUES ('u3', 'test:one', 3, 'user', 'beta', NULL, ?, ?)", - ( - json.dumps({"control_turn_id": "t2", "turn_input_ordinal": 0}), - (started + timedelta(seconds=3)).isoformat(), - ), - ) - connection.execute( - "INSERT INTO messages VALUES ('a2', 'test:one', 4, 'assistant', 'second', NULL, ?, ?)", - ( - json.dumps( - { - "control_turn_id": "t2", - "turn_terminal": True, - "turn_input_count": 1, - } - ), - (started + timedelta(seconds=4)).isoformat(), - ), - ) - embed_started = asyncio.Event() - release_embed = asyncio.Event() - - async def blocked_embed_batch(texts: list[str]) -> list[list[float]]: - embed_started.set() - await release_embed.wait() - return [[1.0, 0.0] if "alpha" in text else [0.0, 1.0] for text in texts] - - monkeypatch.setattr( - engine._embedder, "embed_batch", blocked_embed_batch - ) # noqa: SLF001 - event = TurnCommitted( - session_key="test:one", - channel="test", - chat_id="one", - input_message="beta", - persisted_user_message="beta", - assistant_response="second", - tools_used=[], - persisted_user_message_id="u3", - persisted_user_message_ids=("u3",), - assistant_message_id="a2", - timestamp=started, - ) - commit_task = asyncio.create_task(engine._on_turn_committed(event)) # noqa: SLF001 - await embed_started.wait() - - store = SessionStore(tmp_path / "sessions.db") - deletion_task = asyncio.create_task( - engine.delete_interaction_source( - "t1", - lambda: store.delete_interaction("t1"), - ) - ) - await asyncio.sleep(0) - assert not deletion_task.done() - release_embed.set() - await commit_task - deletion = await deletion_task - assert deletion is not None - - with closing(sqlite3.connect(tmp_path / "sessions.db")) as connection: - assert ( - connection.execute( - "SELECT message_id FROM message_embeddings WHERE message_id IN ('u1', 'u2', 'a1')" - ).fetchall() - == [] - ) - assert connection.execute( - "SELECT message_id FROM message_embeddings WHERE message_id IN ('u3', 'a2') ORDER BY message_id" - ).fetchall() == [("a2",), ("u3",)] - assert [turn.turn_id for turn in engine._runtime.cycle.turns] == [ # noqa: SLF001 - "u3::a2" - ] - store.close() - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -def test_restart_repairs_sidecars_after_source_interaction_was_deleted( - tmp_path: Path, -) -> None: - _seed_two_explicit_akasha_turns(tmp_path) - original = _engine(tmp_path) - original._runtime.close() # noqa: SLF001 - original._embedding_store.close() # noqa: SLF001 - store = SessionStore(tmp_path / "sessions.db") - deletion = store.delete_interaction("t1") - assert deletion is not None - store.close() - - restarted = _engine(tmp_path) - - assert [ - turn.turn_id for turn in restarted._runtime.cycle.turns - ] == [ # noqa: SLF001 - "u3::a2" - ] - restarted._runtime.close() # noqa: SLF001 - restarted._embedding_store.close() # noqa: SLF001 - - -@pytest.mark.asyncio -async def test_failed_interaction_rebuild_keeps_akasha_fail_loud( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - - def fail_rebuild() -> None: - raise OSError("replace failed") - - monkeypatch.setattr( - engine._runtime, # noqa: SLF001 - "rebuild_from_source", - fail_rebuild, - ) - deletion = InteractionDeletion( - control_turn_id="t1", - session_key="test:one", - message_ids=("u1", "a1"), - first_user_message_id="u1", - old_last_consolidated=2, - new_last_consolidated=0, - backup_path=str(tmp_path / "backup.db"), - ) - - with pytest.raises(RuntimeError, match="failed to reconcile"): - await engine.delete_interaction_source("t1", lambda: deletion) - with pytest.raises(RuntimeError, match="derived state is stale"): - engine.list_items_for_dashboard() - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -def test_sparse_builder_preserves_single_user_embedding_bytes( - tmp_path: Path, -) -> None: - """Keep legacy turn digests stable when multi-user projection is enabled.""" - - # 1. Persist a legacy pair whose stored vector is deliberately not normalized. - sessions = tmp_path / "sessions.db" - index = tmp_path / "index.db" - _create_sessions(sessions) - started = datetime(2026, 8, 6, tzinfo=timezone.utc) - with closing(sqlite3.connect(sessions)) as connection, connection: - connection.execute( - "INSERT INTO sessions VALUES ('test:one', ?, ?, 0, NULL)", - (started.isoformat(), started.isoformat()), - ) - for message_id, seq, role, content, vector in ( - ("u1", 0, "user", "legacy", (3.0, 4.0)), - ("a1", 1, "assistant", "final", (0.0, 1.0)), - ): - connection.execute( - "INSERT INTO messages VALUES (?, 'test:one', ?, ?, ?, NULL, NULL, ?)", - ( - message_id, - seq, - role, - content, - (started + timedelta(seconds=seq)).isoformat(), - ), - ) - connection.execute( - "INSERT INTO message_embeddings VALUES (?, ?, 'embedding-model', ?, 2, ?, ?)", - ( - message_id, - hashlib.sha256(content.encode()).hexdigest(), - sqlite3.Binary(struct.pack("<2f", *vector)), - started.isoformat(), - started.isoformat(), - ), - ) - - # 2. The incremental source digest must continue to see the original bytes. - build_sparse_index( - sessions, - index, - BuildConfig(embedding_model="embedding-model", embedding_dimension=2), - ) - with closing(sqlite3.connect(index)) as connection: - dense = connection.execute( - "SELECT embedding FROM turn_dense WHERE field = 'user'" - ).fetchone() - - assert dense is not None - assert dense[0] == struct.pack("<2f", 3.0, 4.0) - - -def _engine( - workspace: Path, - *, - event_publisher: EventBus | None = None, -) -> AkashaMemoryEngine: - return AkashaMemoryEngine( - embeddings=_Embeddings(), - embedding_space=_embedding_space(), - runtime_scope=_runtime_scope, - akasha_config=AkashaConfig(), - workspace=workspace, - event_publisher=event_publisher, - ) - - -def _runtime_handle(engine: AkashaMemoryEngine) -> akasha_plugin._AkashaRuntimeHandle: - handle = akasha_plugin._AkashaRuntimeHandle() - handle.configure( - lambda: engine, - embedding_identity=lambda: engine.embedding_api.model_id, - ) - return handle - - -def _query( - text: str, - timestamp: datetime, - *, - intent: str, -) -> MemoryQuery: - return MemoryQuery( - text=text, - intent=cast(Any, intent), - effect="stateful", - scope=MemoryScope( - session_key="test:one", - channel="test", - chat_id="one", - ), - limit=5, - timestamp=timestamp, - ) - - -def _event( - *, - sequence: int, - user: str, - assistant: str, - started: datetime, - turn_id: str = "", - client_message_id: str = "", -) -> TurnCommitted: - return TurnCommitted( - session_key="test:one", - channel="test", - chat_id="one", - input_message=user, - persisted_user_message=user, - assistant_response=assistant, - tools_used=[], - turn_id=turn_id, - client_message_id=client_message_id, - persisted_user_message_id=f"message:{sequence}", - assistant_message_id=f"message:{sequence + 1}", - timestamp=started, - ) - - -def _create_sessions(path: Path) -> None: - with closing(sqlite3.connect(path)) as connection, connection: - connection.execute(""" - CREATE TABLE sessions ( - key TEXT PRIMARY KEY, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_consolidated INTEGER NOT NULL DEFAULT 0, - metadata TEXT - ) - """) - connection.execute(""" - CREATE TABLE messages ( - id TEXT PRIMARY KEY, - session_key TEXT NOT NULL, - seq INTEGER NOT NULL, - role TEXT NOT NULL, - content TEXT, - tool_chain TEXT, - extra TEXT, - ts TEXT NOT NULL, - UNIQUE(session_key, seq) - ) - """) - connection.execute(""" - CREATE TABLE message_embeddings ( - message_id TEXT NOT NULL, - content_hash TEXT NOT NULL, - model TEXT NOT NULL, - embedding BLOB NOT NULL, - dim INTEGER NOT NULL, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - PRIMARY KEY(message_id, model) - ) - """) - - -def _append_turn( - path: Path, - *, - sequence: int, - user: str, - assistant: str, - started: datetime, - session_key: str = "test:one", - with_embeddings: bool = False, - assistant_tool_chain: str | None = None, - user_extra: dict[str, object] | None = None, - session_metadata: dict[str, object] | None = None, -) -> None: - assistant_time = started + timedelta(seconds=10) - with closing(sqlite3.connect(path)) as connection, connection: - connection.execute( - """ - INSERT INTO sessions (key, created_at, updated_at, last_consolidated, metadata) - VALUES (?, ?, ?, 0, ?) - ON CONFLICT(key) DO UPDATE SET metadata = excluded.metadata - """, - ( - session_key, - started.isoformat(), - assistant_time.isoformat(), - (None if session_metadata is None else json.dumps(session_metadata)), - ), - ) - connection.executemany( - "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - [ - ( - ( - f"{session_key}:{sequence}" - if session_key != "test:one" - else f"message:{sequence}" - ), - session_key, - sequence, - "user", - user, - None, - (None if user_extra is None else json.dumps(user_extra)), - started.isoformat(), - ), - ( - ( - f"{session_key}:{sequence + 1}" - if session_key != "test:one" - else f"message:{sequence + 1}" - ), - session_key, - sequence + 1, - "assistant", - assistant, - assistant_tool_chain, - None, - assistant_time.isoformat(), - ), - ], - ) - if with_embeddings: - for offset, text in enumerate((user, assistant)): - message_id = ( - f"{session_key}:{sequence + offset}" - if session_key != "test:one" - else f"message:{sequence + offset}" - ) - vector = ( - b"\x00\x00\x80?\x00\x00\x00\x00" - if "alpha" in text - else b"\x00\x00\x00\x00\x00\x00\x80?" - ) - connection.execute( - """ - INSERT INTO message_embeddings - VALUES (?, ?, 'embedding-model', ?, 2, ?, ?) - """, - ( - message_id, - hashlib.sha256(text.encode()).hexdigest(), - vector, - started.isoformat(), - started.isoformat(), - ), - ) - - -def _close_engine(engine: AkashaMemoryEngine) -> None: - engine._runtime.close() # noqa: SLF001 - engine._embedding_store.close() # noqa: SLF001 - - -def test_build_sparse_index_excludes_marked_and_scheduler_sessions( - tmp_path: Path, -) -> None: - sessions_path = tmp_path / "sessions.db" - _create_sessions(sessions_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - sessions_path, - sequence=0, - user="normal user", - assistant="normal reply", - started=started, - session_key="telegram:1", - with_embeddings=True, - ) - _append_turn( - sessions_path, - sequence=2, - user="pr noise", - assistant="pr reply", - started=started + timedelta(minutes=1), - session_key="github:owner/repo:pr:1", - with_embeddings=True, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - _append_turn( - sessions_path, - sequence=4, - user="scheduler prompt", - assistant="scheduler reply", - started=started + timedelta(minutes=2), - session_key="scheduler:job-1", - with_embeddings=True, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - - result = build_sparse_index(sessions_path, tmp_path / "index.db") - - # 1. 只有普通 session 成为学习样本,排除计数可见。 - assert result.discovered_turns == 1 - assert result.excluded_memory_turns == 2 - with closing(sqlite3.connect(tmp_path / "index.db")) as connection: - row = connection.execute( - "SELECT value FROM metadata WHERE key='turns_excluded_memory'" - ).fetchone() - assert row[0] == "2" - - -def test_audit_source_embeddings_counts_excluded_memory_turns(tmp_path: Path) -> None: - sessions_path = tmp_path / "sessions.db" - _create_sessions(sessions_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - sessions_path, - sequence=0, - user="pr noise", - assistant="pr reply", - started=started, - session_key="github:owner/repo:pr:1", - with_embeddings=True, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - - audit = audit_source_embeddings(sessions_path, BuildConfig()) - - assert audit.eligible_turns == 0 - assert audit.excluded_memory_turns == 1 - - -def test_build_sparse_index_fails_loud_on_orphan_messages(tmp_path: Path) -> None: - sessions_path = tmp_path / "sessions.db" - _create_sessions(sessions_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - sessions_path, - sequence=0, - user="user", - assistant="reply", - started=started, - ) - # 1. 制造孤儿消息:删除 session 行后重建必须 fail-loud,不得静默消失。 - with closing(sqlite3.connect(sessions_path)) as connection, connection: - connection.execute("DELETE FROM sessions") - - with pytest.raises(ValueError, match="孤儿"): - build_sparse_index(sessions_path, tmp_path / "index.db") - - -def test_build_sparse_index_models_arrival_during_previous_reply_as_overlap( - tmp_path: Path, -) -> None: - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "index.db" - _create_sessions(sessions_path) - started = datetime(2026, 8, 5, 2, 23, tzinfo=timezone.utc) - _append_turn( - sessions_path, - sequence=0, - user="first", - assistant="slow reply", - started=started, - ) - _append_turn( - sessions_path, - sequence=2, - user="arrived while busy", - assistant="second reply", - started=started + timedelta(seconds=5), - ) - - build_sparse_index(sessions_path, index_path) - - with closing(sqlite3.connect(index_path)) as connection: - timing = connection.execute(""" - SELECT response_delta_seconds, idle_gap_seconds, log_idle_gap, - overlap_seconds, log_overlap - FROM time_observations WHERE turn_id = 'message:2::message:3' - """).fetchone() - feature = connection.execute(""" - SELECT value FROM sparse_features - WHERE turn_id = 'message:2::message:3' - AND family = 'time_overlap' AND feature_id = 'test' - """).fetchone() - stats = connection.execute(""" - SELECT idle_gap_count, mean_log_idle_gap - FROM time_stats WHERE channel = 'test' - """).fetchone() - - assert timing == pytest.approx((-5.0, 0.0, 0.0, 5.0, math.log1p(5.0))) - assert feature == pytest.approx((math.log1p(5.0),)) - assert stats == pytest.approx((1, 0.0)) - - -def test_online_runtime_rebuilds_previous_sparse_index_version( - tmp_path: Path, -) -> None: - """Boot rebuilds an obsolete derived index without changing sessions.db.""" - - # 1. Freeze one canonical source turn and an obsolete v9 sidecar. - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - _append_turn( - sessions_path, - sequence=0, - user="alpha request", - assistant="beta reply", - started=datetime(2026, 8, 5, tzinfo=timezone.utc), - with_embeddings=True, - ) - index_path.parent.mkdir(parents=True) - with closing(sqlite3.connect(index_path)) as connection, connection: - connection.execute("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT)") - connection.execute("INSERT INTO metadata VALUES ('index_version', '9')") - source_sha = hashlib.sha256(sessions_path.read_bytes()).hexdigest() - - # 2. Boot through the production runtime and verify the derived pair only. - runtime = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - runtime.close() - - with closing(sqlite3.connect(index_path)) as connection: - assert connection.execute( - "SELECT value FROM metadata WHERE key = 'index_version'" - ).fetchone() == (INDEX_VERSION,) - assert connection.execute("SELECT COUNT(*) FROM sparse_turns").fetchone() == ( - 1, - ) - assert memory_path.is_file() - assert hashlib.sha256(sessions_path.read_bytes()).hexdigest() == source_sha - - -def test_online_runtime_keeps_previous_sidecar_when_version_rebuild_fails( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """A failed version rebuild leaves both canonical input and old sidecar intact.""" - - # 1. Freeze the source and obsolete sidecar before the injected rebuild failure. - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - index_path.parent.mkdir(parents=True) - with closing(sqlite3.connect(index_path)) as connection, connection: - connection.execute("CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT)") - connection.execute("INSERT INTO metadata VALUES ('index_version', '9')") - source_sha = hashlib.sha256(sessions_path.read_bytes()).hexdigest() - index_sha = hashlib.sha256(index_path.read_bytes()).hexdigest() - - def fail_rebuild( - _source_path: Path, - output_path: Path, - _config: BuildConfig, - ) -> object: - if output_path == index_path: - raise SparseIndexRebuildRequired("obsolete test index") - raise RuntimeError("injected candidate rebuild failure") - - monkeypatch.setattr( - "plugins.akasha.application.runtime.build_sparse_index", - fail_rebuild, - ) - - # 2. Startup fails loudly without replacing either protected input. - with pytest.raises(RuntimeError, match="candidate rebuild failure"): - OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - - assert hashlib.sha256(sessions_path.read_bytes()).hexdigest() == source_sha - assert hashlib.sha256(index_path.read_bytes()).hexdigest() == index_sha - assert not memory_path.exists() - - -def test_online_runtime_rebuilds_pair_after_crash_between_sidecar_replaces( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """Boot rejects a new index paired with the previous memory snapshot.""" - - # 1. Publish one valid pair, then change its canonical source in place. - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - _append_turn( - sessions_path, - sequence=0, - user="alpha request", - assistant="beta reply", - started=datetime(2026, 8, 5, tzinfo=timezone.utc), - with_embeddings=True, - ) - runtime = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - with closing(sqlite3.connect(sessions_path)) as connection, connection: - connection.execute( - "UPDATE messages SET content = ? WHERE id = ?", - ("replacement request", "message:0"), - ) - - # 2. Crash after publishing the rebuilt index but before its memory pair. - real_replace = os.replace - - def fail_memory_publication(source: Path, destination: Path) -> None: - if Path(destination) == memory_path: - raise OSError("injected crash between sidecar replaces") - real_replace(source, destination) - - monkeypatch.setattr(os, "replace", fail_memory_publication) - with pytest.raises(OSError, match="between sidecar replaces"): - runtime.rebuild_from_source() - runtime.close() - monkeypatch.setattr(os, "replace", real_replace) - mixed_index_sha = hashlib.sha256(index_path.read_bytes()).hexdigest() - mixed_index_state_sha = sparse_index_state_sha256(index_path) - with closing(sqlite3.connect(memory_path)) as connection: - stale_memory_sha = connection.execute( - "SELECT value FROM metadata WHERE key = 'source_index_sha256'" - ).fetchone() - stale_memory_state_sha = connection.execute( - "SELECT value FROM metadata " "WHERE key = 'source_index_state_sha256'" - ).fetchone() - assert stale_memory_sha != (mixed_index_sha,) - assert stale_memory_state_sha != (mixed_index_state_sha,) - - # 3. The next boot detects the mixed pair and deterministically republishes it. - recovered = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - recovered.close() - final_index_sha = hashlib.sha256(index_path.read_bytes()).hexdigest() - with closing(sqlite3.connect(memory_path)) as connection: - recovered_memory_sha = connection.execute( - "SELECT value FROM metadata WHERE key = 'source_index_sha256'" - ).fetchone() - recovered_memory_state_sha = connection.execute( - "SELECT value FROM metadata " "WHERE key = 'source_index_state_sha256'" - ).fetchone() - assert recovered_memory_sha == (final_index_sha,) - assert recovered_memory_state_sha == (sparse_index_state_sha256(index_path),) - - -def test_online_runtime_reopens_unchanged_sidecars_without_rebuilding( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """正常进程重启必须复用未变化的派生库组合。""" - - # 1. 发布一组完整的稀疏索引与记忆快照。 - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - _append_turn( - sessions_path, - sequence=0, - user="alpha request", - assistant="beta reply", - started=datetime(2026, 8, 5, tzinfo=timezone.utc), - with_embeddings=True, - ) - first = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - first.close() - index_sha = hashlib.sha256(index_path.read_bytes()).hexdigest() - memory_sha = hashlib.sha256(memory_path.read_bytes()).hexdigest() - - # 2. 再次打开同一组合,不得进入全量重建路径。 - def reject_rebuild(_runtime: OnlineMemoryRuntime) -> MemoryCycle: - raise AssertionError("unchanged sidecars must not rebuild") - - monkeypatch.setattr( - OnlineMemoryRuntime, - "_fresh_rebuild_from_source", - reject_rebuild, - ) - reopened = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - reopened.close() - - assert hashlib.sha256(index_path.read_bytes()).hexdigest() == index_sha - assert hashlib.sha256(memory_path.read_bytes()).hexdigest() == memory_sha - - -def test_online_runtime_ignores_excluded_turn_diagnostics_on_restart( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """排除计数增长不得触发既有记忆图的全量重放。""" - - # 1. 发布一个旧格式快照,随后只新增明确排除的 session。 - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - started = datetime(2026, 8, 5, tzinfo=timezone.utc) - _append_turn( - sessions_path, - sequence=0, - user="alpha request", - assistant="beta reply", - started=started, - with_embeddings=True, - ) - first = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - first.close() - with closing(sqlite3.connect(memory_path)) as connection, connection: - connection.execute("DELETE FROM metadata WHERE key='source_index_state_sha256'") - _append_turn( - sessions_path, - sequence=0, - user="programmatic request", - assistant="programmatic reply", - started=started + timedelta(minutes=1), - session_key="github:owner/repo:pr:1", - with_embeddings=True, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - - # 2. 重启只更新诊断计数,并把旧快照升级为逻辑索引身份。 - def reject_rebuild(_runtime: OnlineMemoryRuntime) -> MemoryCycle: - raise AssertionError("excluded turns must not rebuild learned memory") - - monkeypatch.setattr( - OnlineMemoryRuntime, - "_fresh_rebuild_from_source", - reject_rebuild, - ) - reopened = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - try: - assert reopened.cycle.state_version == 1 - finally: - reopened.close() - - with closing(sqlite3.connect(index_path)) as connection: - excluded = connection.execute( - "SELECT value FROM metadata WHERE key='turns_excluded_memory'" - ).fetchone() - with closing(sqlite3.connect(memory_path)) as connection: - state_hash = connection.execute( - "SELECT value FROM metadata WHERE key='source_index_state_sha256'" - ).fetchone() - assert excluded == ("1",) - assert state_hash is not None - - # 3. 已迁移快照再次遇到排除计数增长,仍只更新诊断元数据。 - _append_turn( - sessions_path, - sequence=0, - user="scheduler request", - assistant="scheduler reply", - started=started + timedelta(minutes=2), - session_key="scheduler:job-1", - with_embeddings=True, - user_extra={"effects": {"post_commit": "suppress"}}, - ) - modern = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - try: - assert modern.cycle.state_version == 1 - finally: - modern.close() - with closing(sqlite3.connect(index_path)) as connection: - modern_excluded = connection.execute( - "SELECT value FROM metadata WHERE key='turns_excluded_memory'" - ).fetchone() - assert modern_excluded == ("2",) - - -def test_online_runtime_replays_an_appended_suffix_without_rebuilding( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """进程离线期间新增的 Turn 必须增量追平而不是重建完整图。""" - - # 1. 先持久化一个与稀疏索引完全对齐的记忆快照。 - sessions_path = tmp_path / "sessions.db" - index_path = tmp_path / "memory" / "akasha-v2-index.db" - memory_path = tmp_path / "memory" / "akasha.db" - _create_sessions(sessions_path) - _append_turn( - sessions_path, - sequence=0, - user="alpha request", - assistant="beta reply", - started=datetime(2026, 8, 5, tzinfo=timezone.utc), - with_embeddings=True, - ) - first = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - first.close() - - # 2. 模拟 Core 停机窗口新增一个合法 append-only Turn。 - _append_turn( - sessions_path, - sequence=2, - user="gamma request", - assistant="delta reply", - started=datetime(2026, 8, 5, 0, 1, tzinfo=timezone.utc), - with_embeddings=True, - ) - - def reject_rebuild(_runtime: OnlineMemoryRuntime) -> MemoryCycle: - raise AssertionError("append-only suffix must not rebuild") - - monkeypatch.setattr( - OnlineMemoryRuntime, - "_fresh_rebuild_from_source", - reject_rebuild, - ) - - # 3. 重启只回放新增后缀,并发布新的完整配对快照。 - reopened = OnlineMemoryRuntime( - sessions_path=sessions_path, - index_path=index_path, - memory_path=memory_path, - embedding_model="embedding-model", - embedding_dimension=2, - config=MemoryConfig(), - ) - try: - assert reopened.cycle.state_version == 2 - assert len(reopened.cycle.turns) == 2 - finally: - reopened.close() - - -@pytest.mark.parametrize( - ("context_mass", "query", "context", "expected_node"), - ( - (1.0, np.asarray([1.0, 0.0]), np.zeros(2), 0), - (0.0, np.zeros(2), np.asarray([0.0, 1.0]), 1), - ), -) -def test_burst_seed_keeps_the_only_available_evidence_source( - context_mass: float, - query: np.ndarray, - context: np.ndarray, - expected_node: int, -) -> None: - """An unavailable source cannot erase the other side of the mixture.""" - - seed = features_module._mix_sources( # pyright: ignore[reportPrivateUsage] - { - "query_dense": query, - "query_bm25": np.zeros(2), - "context_dense": context, - "context_bm25": np.zeros(2), - }, - context_mass, - ) - - assert seed == ((expected_node, 1.0),) - - -def _write_inspector_config(workspace: Path) -> None: - plugin_dir = builtin_plugin_data_dir("akasha", workspace) - ensure_workspace_plugin_data_dir(plugin_dir, workspace) - (plugin_dir / "config.local.toml").touch() - - -def test_inspector_overview_is_empty_before_first_akasha_commit(tmp_path: Path) -> None: - _write_inspector_config(tmp_path) - reader = AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=load_akasha_config( - builtin_plugin_data_dir("akasha", tmp_path) / "config.local.toml" - ), - ) - sidecars = (reader.paths.memory, reader.paths.index) - - assert reader.get_overview() == { - "available": True, - "total": 0, - "latest_at": None, - "earliest_at": None, - } - assert reader.list_turns() == ([], 0) - assert reader.latest_for_session("fresh:empty") is None - assert reader.get_turn("missing") is None - assert reader.for_assistant_message("fresh:empty", "missing") is None - assert all(not path.exists() for path in sidecars) - - -def test_inspector_accepts_valid_empty_sparse_projection_without_memory( - tmp_path: Path, -) -> None: - _write_inspector_config(tmp_path) - reader = AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=load_akasha_config( - builtin_plugin_data_dir("akasha", tmp_path) / "config.local.toml" - ), - ) - reader.paths.index.parent.mkdir(parents=True, exist_ok=True) - with closing(sqlite3.connect(reader.paths.index)) as connection: - connection.executescript(SCHEMA) - connection.executemany( - "INSERT INTO metadata(key, value) VALUES (?, ?)", - ( - ("index_version", INDEX_VERSION), - ("tool_chain_projection_version", TOOL_CHAIN_PROJECTION_VERSION), - ), - ) - connection.commit() - - assert reader.get_overview() == { - "available": True, - "total": 0, - "latest_at": None, - "earliest_at": None, - } - assert reader.list_turns() == ([], 0) - assert reader.latest_for_session("fresh:empty") is None - assert reader.get_turn("missing") is None - assert reader.for_assistant_message("fresh:empty", "missing") is None - assert not reader.paths.memory.exists() - - -@pytest.mark.parametrize("present", ("memory", "index")) -def test_inspector_partial_sidecar_fails_loud( - tmp_path: Path, - present: str, -) -> None: - _write_inspector_config(tmp_path) - reader = AkashaInspectorReader( - memory_root=tmp_path / "memory", - config=load_akasha_config( - builtin_plugin_data_dir("akasha", tmp_path) / "config.local.toml" - ), - ) - present_path = getattr(reader.paths, present) - present_path.parent.mkdir(parents=True, exist_ok=True) - sqlite3.connect(present_path).close() - - with pytest.raises(sqlite3.OperationalError): - reader.list_turns() - - -@pytest.mark.asyncio -async def test_concurrent_queries_in_one_turn_pair_distinct_spans( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """同一 turn 并发两次 query:span 独立,各自 total/子阶段同 span 且闭合。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - try: - results = await asyncio.gather( - engine.query(_query("alpha one", started, intent="context")), - engine.query(_query("beta two", started, intent="context")), - ) - assert len(results) == 2 - triples = _milestone_triples(caplog) - span_ids = sorted({span for span, _, _ in triples}) - assert len(span_ids) == 2 - for span_id in span_ids: - _assert_span_closed(caplog, span_id, "query") - for span_id, operation, event in triples: - assert operation == "query" - assert sum(1 for _, _, event in triples if event == "akasha.query.start") == 2 - assert sum(1 for _, _, event in triples if event == "akasha.query.done") == 2 - finally: - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) - - -@pytest.mark.asyncio -async def test_query_and_same_turn_commit_use_distinct_spans_and_operations( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - caplog: pytest.LogCaptureFixture, -) -> None: - """同 turn 的 query 与 turn_commit:span 不同,embed 可按 operation 区分。""" - - caplog.set_level(logging.INFO, logger="plugins.akasha.engine") - _create_sessions(tmp_path / "sessions.db") - engine = _engine(tmp_path) - started = datetime(2026, 7, 6, 8, tzinfo=timezone.utc) - _append_turn( - tmp_path / "sessions.db", - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - ) - session_token = current_session_key.set("test:one") - client_token = current_client_message_id.set("client-message-1") - turn_token = running_turn_id.set("turn-1") - try: - await engine._on_turn_committed( # noqa: SLF001 - _event( - sequence=0, - user="alpha start", - assistant="first answer", - started=started, - turn_id="event-turn-1", - client_message_id="event-client-1", - ) - ) - await engine._wait_for_publication() # noqa: SLF001 - await engine.query( - _query( - "alpha follow", - started + timedelta(minutes=5), - intent="context", - ) - ) - triples = _milestone_triples(caplog) - spans = {(span, operation) for span, operation, _ in triples} - assert len(spans) == 2 - assert sorted(operation for _, operation in spans) == [ - "query", - "turn_commit", - ] - query_span = next(span for span, operation in spans if operation == "query") - commit_span = next( - span for span, operation in spans if operation == "turn_commit" - ) - assert query_span != commit_span - _assert_span_closed(caplog, query_span, "query") - _assert_span_closed(caplog, commit_span, "turn_commit") - # 同一事件名 akasha.embed.* 可按 operation 区分归属。 - assert { - (span, operation) - for span, operation, event in triples - if event == "akasha.embed.start" - } == {(query_span, "query"), (commit_span, "turn_commit")} - assert { - operation - for _, operation, event in triples - if event.startswith("akasha.embed.") - } == {"query", "turn_commit"} - finally: - current_session_key.reset(session_token) - current_client_message_id.reset(client_token) - running_turn_id.reset(turn_token) - _close_engine(engine) diff --git a/tests/test_akashic_channel.py b/tests/test_akashic_channel.py deleted file mode 100644 index 15dd91edd..000000000 --- a/tests/test_akashic_channel.py +++ /dev/null @@ -1,360 +0,0 @@ -from types import SimpleNamespace -from pathlib import Path -from typing import Any, cast - -import pytest - -from agent.plugin_composition.channels import ( - ChannelDeliveryReceipt, - ChannelFactoryContext, - ChannelReady, - DeliveryStatus, - ProviderDeliveryReceipt, - ProviderDeliveryRequest, - StopReceipt, -) -from agent.control.scoped_turn import TurnAcceptedReceipt -from agent.plugin_composition.durable_deliveries import ( - DurableBindingAttempt, - DurableDeliveryRequest, - PluginDurableDeliveries, -) -from agent.plugin_composition.durable_delivery_store import DurableDeliveryStore -from infra.channels.akashic_channel import AkashicChannel -from session.manager import SessionManager - - -class _Adapter: - def __init__( - self, - binding_token: str, - status: DeliveryStatus, - *, - error: BaseException | None = None, - provider_ids: tuple[str, ...] | None = None, - ) -> None: - self.binding_token = binding_token - self.status = status - self.error = error - self.provider_ids = provider_ids - self.requests: list[ProviderDeliveryRequest] = [] - self.runtime: object | None = None - self.admission_open = False - - async def start(self) -> ChannelReady: - return ChannelReady(self.binding_token) - - def attach_runtime(self, ports: object) -> None: - self.runtime = ports - - def open_admission(self) -> None: - self.admission_open = True - - def close_admission(self) -> None: - self.admission_open = False - - async def deliver( - self, - request: ProviderDeliveryRequest, - ) -> ProviderDeliveryReceipt: - self.requests.append(request) - if self.error is not None: - raise self.error - return ProviderDeliveryReceipt( - request.delivery_id, - self.status, - provider_ids=self.provider_ids or (f"{self.status.value}-id",), - error=( - "unavailable" if self.status is not DeliveryStatus.DELIVERED else None - ), - ) - - async def stop(self) -> StopReceipt: - return StopReceipt(self.binding_token, resources_closed=True) - - -class _Child: - name = "transport-detail" - - def __init__( - self, - status: DeliveryStatus, - *, - error: BaseException | None = None, - provider_ids: tuple[str, ...] | None = None, - ) -> None: - self.status = status - self.error = error - self.provider_ids = provider_ids - self.adapter: _Adapter | None = None - - def build_v3_adapter(self, context: ChannelFactoryContext) -> _Adapter: - self.adapter = _Adapter( - context.binding_token, - self.status, - error=self.error, - provider_ids=self.provider_ids, - ) - return self.adapter - - -class _FailingLifecycleAdapter(_Adapter): - def __init__( - self, - binding_token: str, - *, - fail_start: bool = False, - fail_stop: bool = False, - fail_open: bool = False, - fail_close: bool = False, - ) -> None: - super().__init__(binding_token, DeliveryStatus.DELIVERED) - self.fail_start = fail_start - self.fail_stop = fail_stop - self.fail_open = fail_open - self.fail_close = fail_close - self.close_calls = 0 - - async def start(self) -> ChannelReady: - if self.fail_start: - raise RuntimeError("start failed") - return await super().start() - - async def stop(self) -> StopReceipt: - if self.fail_stop: - raise RuntimeError("stop failed") - return await super().stop() - - def open_admission(self) -> None: - if self.fail_open: - raise RuntimeError("open failed") - super().open_admission() - - def close_admission(self) -> None: - self.close_calls += 1 - if self.fail_close: - raise RuntimeError("close failed") - super().close_admission() - - -class _LifecycleChild: - name = "transport-detail" - - def __init__(self, adapter: _FailingLifecycleAdapter) -> None: - self.adapter = adapter - - def build_v3_adapter( - self, context: ChannelFactoryContext - ) -> _FailingLifecycleAdapter: - return self.adapter - - -def _context() -> ChannelFactoryContext: - return ChannelFactoryContext( - snapshot_id="snapshot", - generation_id="generation", - binding_token="binding", - config={}, - credentials={}, - provider_client_factory=cast(Any, SimpleNamespace()), - ingress=None, - identity=None, - ) - - -def _request() -> ProviderDeliveryRequest: - return ProviderDeliveryRequest( - binding_token="binding", - delivery_id="delivery", - recipient="chat-id", - body="hello", - ) - - -@pytest.mark.asyncio -async def test_projects_one_core_delivery_to_web_and_mobile() -> None: - web = _Child(DeliveryStatus.DELIVERED) - mobile = _Child(DeliveryStatus.REJECTED) - channel = AkashicChannel(cast(Any, web), cast(Any, mobile)) - adapter = channel.build_v3_adapter(_context()) - - receipt = await adapter.deliver(_request()) - - assert channel.name == "akashic" - assert receipt.status is DeliveryStatus.DELIVERED - assert web.adapter is not None and len(web.adapter.requests) == 1 - assert mobile.adapter is not None and len(mobile.adapter.requests) == 1 - assert web.adapter.requests[0] is mobile.adapter.requests[0] - - -@pytest.mark.asyncio -async def test_preserves_unknown_when_another_client_delivered() -> None: - web = _Child(DeliveryStatus.DELIVERED) - mobile = _Child(DeliveryStatus.UNKNOWN, error=RuntimeError("offline")) - adapter = AkashicChannel(cast(Any, web), cast(Any, mobile)).build_v3_adapter( - _context() - ) - - receipt = await adapter.deliver(_request()) - - assert receipt.status is DeliveryStatus.UNKNOWN - assert "offline" in str(receipt.error) - - -@pytest.mark.asyncio -async def test_provider_ids_are_unique_in_first_seen_order() -> None: - web = _Child( - DeliveryStatus.DELIVERED, - provider_ids=("shared", "web"), - ) - mobile = _Child( - DeliveryStatus.DELIVERED, - provider_ids=("shared", "mobile", "web"), - ) - adapter = AkashicChannel(cast(Any, web), cast(Any, mobile)).build_v3_adapter( - _context() - ) - - receipt = await adapter.deliver(_request()) - - assert receipt.provider_ids == ("shared", "web", "mobile") - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("web_status", "mobile_status", "expected_state", "expected_messages"), - ( - (DeliveryStatus.DELIVERED, DeliveryStatus.DELIVERED, "projected", 1), - (DeliveryStatus.DELIVERED, DeliveryStatus.REJECTED, "projected", 1), - (DeliveryStatus.DELIVERED, DeliveryStatus.UNKNOWN, "uncertain", 0), - ), -) -async def test_durable_projection_follows_composite_delivery_matrix( - tmp_path: Path, - web_status: DeliveryStatus, - mobile_status: DeliveryStatus, - expected_state: str, - expected_messages: int, -) -> None: - """Unknown blocks projection; one delivered UI is enough after rejection.""" - - web = _Child(web_status) - mobile = _Child(mobile_status) - adapter = AkashicChannel(cast(Any, web), cast(Any, mobile)).build_v3_adapter( - _context() - ) - sessions = SessionManager(tmp_path / "workspace") - store = DurableDeliveryStore(tmp_path / "settlements.sqlite") - provider_calls = 0 - - async def sender(request: DurableDeliveryRequest, started: Any) -> Any: - nonlocal provider_calls - provider_calls += 1 - started(DurableBindingAttempt("attempt", "snapshot", "generation", "binding")) - receipt = await adapter.deliver( - ProviderDeliveryRequest( - binding_token="binding", - delivery_id=request.logical_delivery_id, - recipient=request.recipient, - body=request.body, - ) - ) - return ChannelDeliveryReceipt( - receipt.delivery_id, - receipt.status, - receipt.provider_ids, - receipt.error, - ) - - async def project(request: DurableDeliveryRequest) -> str: - return await sessions.append_durable_delivery( - session_key=request.projection_session_id, - content=request.body, - delivery_id=request.logical_delivery_id, - control_turn_id=request.accepted_turn.turn_id, - ) - - request = DurableDeliveryRequest( - logical_delivery_id="schedule:matrix", - accepted_turn=TurnAcceptedReceipt("scheduler:job", "turn:matrix"), - target_service="scheduler.delivery.v1", - channel="akashic", - recipient="a" * 32, - projection_session_id="akashic:" + "a" * 32, - body="scheduled result", - ) - service = PluginDurableDeliveries(store, sender, project) - - result = await service.submit(request) - duplicate = await service.submit(request) - - assert result.state == duplicate.state == expected_state - assert provider_calls == 1 - assert ( - len( - sessions.control_store.fetch_session_messages(request.projection_session_id) - ) - == expected_messages - ) - assert web.adapter is not None and len(web.adapter.requests) == 1 - assert mobile.adapter is not None and len(mobile.adapter.requests) == 1 - sessions.close() - - -@pytest.mark.asyncio -async def test_delegates_one_binding_lifecycle_to_both_adapters() -> None: - web = _Child(DeliveryStatus.DELIVERED) - mobile = _Child(DeliveryStatus.DELIVERED) - adapter = AkashicChannel(cast(Any, web), cast(Any, mobile)).build_v3_adapter( - _context() - ) - runtime = SimpleNamespace() - - ready = await adapter.start() - adapter.attach_runtime(cast(Any, runtime)) - adapter.open_admission() - adapter.close_admission() - stopped = await adapter.stop() - - assert ready.binding_token == "binding" - assert stopped.resources_closed is True - assert web.adapter is not None and web.adapter.runtime is runtime - assert mobile.adapter is not None and mobile.adapter.runtime is runtime - assert web.adapter.admission_open is False - assert mobile.adapter.admission_open is False - - -@pytest.mark.asyncio -async def test_start_rollback_preserves_primary_and_stop_failure() -> None: - first = _FailingLifecycleAdapter("binding", fail_stop=True) - second = _FailingLifecycleAdapter("binding", fail_start=True) - adapter = AkashicChannel( - cast(Any, _LifecycleChild(first)), - cast(Any, _LifecycleChild(second)), - ).build_v3_adapter(_context()) - - with pytest.raises(BaseExceptionGroup) as raised: - await adapter.start() - - assert {str(error) for error in raised.value.exceptions} == { - "start failed", - "stop failed", - } - - -def test_admission_rollback_closes_every_open_child_and_preserves_failures() -> None: - first = _FailingLifecycleAdapter("binding", fail_close=True) - second = _FailingLifecycleAdapter("binding", fail_open=True) - adapter = AkashicChannel( - cast(Any, _LifecycleChild(first)), - cast(Any, _LifecycleChild(second)), - ).build_v3_adapter(_context()) - - with pytest.raises(BaseExceptionGroup) as raised: - adapter.open_admission() - - assert first.close_calls == 1 - assert {str(error) for error in raised.value.exceptions} == { - "open failed", - "close failed", - } diff --git a/tests/test_akashic_release_installer.py b/tests/test_akashic_release_installer.py deleted file mode 100644 index aa5fa18bb..000000000 --- a/tests/test_akashic_release_installer.py +++ /dev/null @@ -1,709 +0,0 @@ -from __future__ import annotations - -import json -import subprocess -import sys -from pathlib import Path -from types import SimpleNamespace - -import pytest - -from scripts.akashic_release import activate as activate_module -from scripts.akashic_release import prepare as prepare_module -from scripts.akashic_release import systemd as systemd_module -from scripts.akashic_release.activate import activate_release, render_environment -from scripts.akashic_release.activate import release_environment -from scripts.akashic_release.bridge import prepare_bridge_venv -from scripts.akashic_release.doctor import probe_bridge, read_environment -from scripts.akashic_release.doctor import release_health_timeout -from scripts.akashic_release.manifest import read_json, release_lock, write_json -from scripts.akashic_release.migrate import migration_plan -from scripts.akashic_release.model import ReleasePaths -from scripts.akashic_release.prepare import prepare_generation -from scripts.akashic_release.source import resolve_target -from scripts.akashic_release.source import verify_bootstrap_checkout -from scripts.akashic_release.systemd import install_units -from scripts.akashic_release.systemd import install_operator_entrypoint -from scripts.akashic_release.systemd import verify_external_service_contract - - -def _repository(tmp_path: Path) -> tuple[Path, Path, str, str]: - source = tmp_path / "source" - source.mkdir() - subprocess.run(["git", "init", "-q", "-b", "main"], cwd=source, check=True) - subprocess.run( - ["git", "config", "user.email", "test@example.invalid"], - cwd=source, - check=True, - ) - subprocess.run(["git", "config", "user.name", "Test"], cwd=source, check=True) - (source / "value.txt").write_text("one\n", encoding="utf-8") - subprocess.run(["git", "add", "."], cwd=source, check=True) - subprocess.run(["git", "commit", "-qm", "one"], cwd=source, check=True) - first = subprocess.check_output( - ["git", "rev-parse", "HEAD"], cwd=source, text=True - ).strip() - subprocess.run(["git", "tag", "kept"], cwd=source, check=True) - (source / "value.txt").write_text("two\n", encoding="utf-8") - subprocess.run(["git", "commit", "-qam", "two"], cwd=source, check=True) - second = subprocess.check_output( - ["git", "rev-parse", "HEAD"], cwd=source, text=True - ).strip() - remote = tmp_path / "remote.git" - subprocess.run( - ["git", "clone", "-q", "--bare", str(source), str(remote)], check=True - ) - return source, remote, first, second - - -def test_source_resolves_latest_main_and_explicit_reachable_commit( - tmp_path: Path, -) -> None: - _source, remote, first, second = _repository(tmp_path) - - assert resolve_target(str(remote), None, run=subprocess.run) == second - assert resolve_target(str(remote), first, run=subprocess.run) == first - with pytest.raises(RuntimeError, match="40 位"): - resolve_target(str(remote), "main", run=subprocess.run) - - -def test_bootstrap_checkout_requires_exact_clean_origin(tmp_path: Path) -> None: - source, remote, _first, second = _repository(tmp_path) - subprocess.run( - ["git", "remote", "add", "origin", str(remote)], cwd=source, check=True - ) - - verify_bootstrap_checkout(source, second, str(remote), run=subprocess.run) - (source / "dirty.txt").write_text("dirty", encoding="utf-8") - with pytest.raises(RuntimeError, match="clean"): - verify_bootstrap_checkout(source, second, str(remote), run=subprocess.run) - - -def test_release_lock_rejects_concurrent_installer(tmp_path: Path) -> None: - lock = tmp_path / "release.lock" - with release_lock(lock): - with pytest.raises(RuntimeError, match="已有"): - with release_lock(lock): - pass - - -def test_prepare_failure_removes_only_owned_partial_generation( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - paths = ReleasePaths(tmp_path / "root") - paths.create_layout() - commit = "a" * 40 - - def checkout(_bootstrap: Path, _commit: str, target: Path, _origin: str) -> Path: - target.mkdir() - return target - - def image(**kwargs: object) -> dict[str, object]: - Path(str(kwargs["manifest"])).write_text("{}", encoding="utf-8") - return { - "sourceCommit": commit, - "imageId": "sha256:" + "b" * 64, - "hostToolchainIdentity": {"toolchainDigest": "c" * 64}, - } - - def bridge(**kwargs: object) -> Path: - target = Path(str(kwargs["target"])) - (target / "bin").mkdir(parents=True) - python = target / "bin/python" - python.write_text("", encoding="utf-8") - return python - - monkeypatch.setattr(prepare_module, "prepare_runtime_checkout", checkout) - monkeypatch.setattr(prepare_module, "prepare_core_image", image) - monkeypatch.setattr(prepare_module, "prepare_bridge_venv", bridge) - monkeypatch.setattr( - prepare_module, - "verify_bridge", - lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("doctor failed")), - ) - - with pytest.raises(RuntimeError, match="doctor failed"): - prepare_generation( - paths=paths, - bootstrap_checkout=tmp_path, - commit=commit, - origin="origin", - mise=tmp_path / "mise", - run=subprocess.run, - ) - - assert not paths.source(commit).exists() - assert not paths.bridge_venv(commit).exists() - assert not paths.release(commit).exists() - - -def test_bridge_venv_uses_hashed_requirements_from_domestic_index( - tmp_path: Path, -) -> None: - checkout = tmp_path / "checkout" - requirements = checkout / "docker/host-runtime/requirements.lock" - requirements.parent.mkdir(parents=True) - requirements.write_text("", encoding="utf-8") - target = tmp_path / "bridge-venv" - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - if arguments[-2:] == ["which", "python"]: - return subprocess.CompletedProcess( - arguments, - 0, - stdout="/mise/python/3.14.6/bin/python\n", - ) - if "venv" in arguments: - (target / "bin").mkdir(parents=True) - (target / "bin/python").write_text("", encoding="utf-8") - return subprocess.CompletedProcess(arguments, 0) - - python = prepare_bridge_venv( - checkout=checkout, - target=target, - mise=tmp_path / "mise", - run=run, - ) - - install = calls[-1] - assert python == target / "bin/python" - assert install[install.index("--default-index") + 1] == ( - "https://mirrors.aliyun.com/pypi/simple" - ) - assert "--require-hashes" in install - create = calls[-2] - assert create[create.index("--python") + 1] == ( - "/mise/python/3.14.6/bin/python" - ) - - -def test_bridge_probe_uses_generation_python_without_secret_arguments( - tmp_path: Path, -) -> None: - environment_file = tmp_path / "runtime.env" - bridge_python = tmp_path / "bridge-venv/bin/python" - checkout = tmp_path / "runtime-source" - environment = { - "AKASHIC_BRIDGE_PYTHON": str(bridge_python), - "AKASHIC_RUNTIME_CHECKOUT": str(checkout), - "AKASHIC_HOST_BRIDGE_TOKEN": "must-not-enter-argv", - } - calls: list[tuple[list[str], dict[str, object]]] = [] - - def run(arguments: list[str], **kwargs: object) -> subprocess.CompletedProcess[str]: - calls.append((arguments, kwargs)) - return subprocess.CompletedProcess(arguments, 0) - - probe_bridge(environment, environment_file=environment_file, run=run) - - assert calls == [ - ( - [ - str(bridge_python), - "-m", - "scripts.akashic_release.doctor", - "--bridge-probe-environment", - str(environment_file), - ], - { - "cwd": checkout, - "check": True, - "capture_output": True, - "text": True, - }, - ) - ] - assert "must-not-enter-argv" not in " ".join(calls[0][0]) - - -def test_release_health_timeout_uses_core_readiness_owner() -> None: - assert release_health_timeout({}) == 180.0 - assert release_health_timeout({"AKASHIC_READINESS_TIMEOUT_S": "1800"}) == 1860.0 - - -@pytest.mark.parametrize("value", ["0", "-1", "nan", "infinity", "invalid"]) -def test_release_health_timeout_rejects_invalid_config(value: str) -> None: - with pytest.raises(RuntimeError, match="必须是正数"): - release_health_timeout({"AKASHIC_READINESS_TIMEOUT_S": value}) - - -def test_activation_failure_atomically_restores_previous_environment( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(activate_module, "docker_socket_gid", lambda: 961) - paths = ReleasePaths(tmp_path / "root") - paths.create_layout() - (paths.state / "config.toml").write_text("[runtime]\n", encoding="utf-8") - (paths.state / "workspace").mkdir() - (paths.state / "plugin-home").mkdir() - old = "a" * 40 - target = "b" * 40 - write_json( - paths.activation / "active.json", - {"schemaVersion": 1, "status": "active", "targetCommit": old}, - ) - manifest = paths.release(target) - write_json( - manifest, - { - "sourceCommit": target, - "imageId": "sha256:" + "c" * 64, - "hostToolchainIdentity": {"toolchainDigest": "d" * 64}, - }, - ) - environment = tmp_path / "runtime.env" - original = "AKASHIC_RUNTIME_COMMIT=" + old + "\nOPENCODE_GO_API_KEY=secret\n" - environment.write_text(original, encoding="utf-8") - calls = 0 - - def verify(_environment: Path) -> None: - nonlocal calls - calls += 1 - if calls == 1: - raise RuntimeError("candidate unhealthy") - - monkeypatch.setattr(activate_module, "verify_release", verify) - fake_run = lambda arguments, **_kwargs: subprocess.CompletedProcess(arguments, 0) - - with pytest.raises(RuntimeError, match="已恢复"): - activate_release( - paths=paths, - manifest_path=manifest, - environment_file=environment, - mise=tmp_path / "mise", - run=fake_run, - ) - - assert environment.read_text(encoding="utf-8") == original - assert read_json(paths.activation / "active.json")["targetCommit"] == old - failed = list(paths.activation.glob(f"failed-{target}-*.json")) - assert len(failed) == 1 - assert read_json(failed[0])["status"] == "rolled_back" - - -def test_previous_recovery_failure_records_maintenance_receipt( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr(activate_module, "docker_socket_gid", lambda: 961) - paths = ReleasePaths(tmp_path / "root") - paths.create_layout() - (paths.state / "config.toml").write_text("[runtime]\n", encoding="utf-8") - (paths.state / "workspace").mkdir() - (paths.state / "plugin-home").mkdir() - previous = "a" * 40 - target = "b" * 40 - write_json( - paths.activation / "active.json", - {"schemaVersion": 1, "status": "active", "targetCommit": previous}, - ) - manifest = paths.release(target) - write_json( - manifest, - { - "sourceCommit": target, - "imageId": "sha256:" + "c" * 64, - "hostToolchainIdentity": {"toolchainDigest": "d" * 64}, - }, - ) - environment = tmp_path / "runtime.env" - original = f"AKASHIC_RUNTIME_COMMIT={previous}\nOPENCODE_GO_API_KEY=secret\n" - environment.write_text(original, encoding="utf-8") - verify_errors = iter(("candidate unhealthy", "previous unhealthy")) - monkeypatch.setattr( - activate_module, - "verify_release", - lambda _environment: (_ for _ in ()).throw(RuntimeError(next(verify_errors))), - ) - service_calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - service_calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - with pytest.raises(RuntimeError, match="均验证失败.*人工恢复"): - activate_release( - paths=paths, - manifest_path=manifest, - environment_file=environment, - mise=tmp_path / "mise", - run=run, - ) - - assert environment.read_text(encoding="utf-8") == original - receipt_path = next(paths.activation.glob(f"failed-{target}-*.json")) - receipt = read_json(receipt_path) - assert receipt["status"] == "recovery_failed" - assert receipt["detail"] == "candidate unhealthy" - assert receipt["recoveryDetail"] == "previous unhealthy" - assert receipt["previousCommit"] == previous - assert receipt["manualCommands"] == [ - "sudo systemctl stop akashic-core.service akashic-host-bridge.service", - "sudo systemctl start akashic-host-bridge.service akashic-core.service", - f"AKASHIC_RUNTIME_ENV={environment} akashic-release doctor", - ] - assert service_calls[-1] == [ - "sudo", - "systemctl", - "stop", - "akashic-core.service", - "akashic-host-bridge.service", - ] - - -def test_runtime_environment_rejects_multiline_secret(tmp_path: Path) -> None: - with pytest.raises(RuntimeError, match="换行"): - render_environment({"TOKEN": "first\nsecond"}) - - -def test_release_environment_preserves_web_bind_and_loopback_mobile_port( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - paths = ReleasePaths(tmp_path / "root") - paths.create_layout() - commit = "a" * 40 - monkeypatch.setattr("scripts.akashic_release.activate.docker_socket_gid", lambda: 961) - environment = release_environment( - paths=paths, - manifest={ - "sourceCommit": commit, - "imageId": "sha256:" + "b" * 64, - "hostToolchainIdentity": {"toolchainDigest": "c" * 64}, - }, - current={ - "AKASHIC_WEB_BIND_ADDRESS": "192.168.0.100", - "OPENCODE_GO_API_KEY": "secret", - }, - mise=tmp_path / "mise", - ) - - compose = Path("docker/host-runtime/compose.experiment.yaml").read_text() - assert environment["AKASHIC_WEB_BIND_ADDRESS"] == "192.168.0.100" - assert environment["AKASHIC_PUBLISHED_MOBILE_PORT"] == "6323" - assert environment["AKASHIC_DOCKER_GID"] == "961" - assert ( - '"${AKASHIC_WEB_BIND_ADDRESS:-127.0.0.1}:' - '${AKASHIC_PUBLISHED_WEB_PORT:-2236}:2236"' in compose - ) - assert '127.0.0.1:${AKASHIC_PUBLISHED_MOBILE_PORT:-6323}:6323' in compose - assert 'TZ: "${TZ:-Asia/Shanghai}"' in compose - assert 'start_period: "${AKASHIC_READINESS_TIMEOUT_S:-120}s"' in compose - assert 'user: "${AKASHIC_UID:-1000}:${AKASHIC_GID:-1000}"' in compose - assert '"${AKASHIC_DOCKER_GID:?AKASHIC_DOCKER_GID is required}"' in compose - assert "- --socket-uid" in compose - - -def test_activation_rejects_unadopted_legacy_skill_before_stopping( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - paths = ReleasePaths(tmp_path / "root") - paths.create_layout() - (paths.state / "config.toml").write_text("[runtime]\n", encoding="utf-8") - workspace = paths.state / "workspace" - skills = workspace / "skills" - skills.mkdir(parents=True) - plugin_home = paths.state / "plugin-home" - target = plugin_home / "cache/plugin/skills/legacy" - target.mkdir(parents=True) - (skills / "legacy").symlink_to(target, target_is_directory=True) - commit = "b" * 40 - manifest = paths.release(commit) - write_json( - manifest, - { - "sourceCommit": commit, - "imageId": "sha256:" + "c" * 64, - "hostToolchainIdentity": {"toolchainDigest": "d" * 64}, - }, - ) - calls: list[list[str]] = [] - monkeypatch.setenv("OPENCODE_GO_API_KEY", "test-secret") - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - with pytest.raises(RuntimeError, match="legacy skill links"): - activate_release( - paths=paths, - manifest_path=manifest, - environment_file=tmp_path / "runtime.env", - mise=tmp_path / "mise", - run=run, - ) - - assert calls == [] - - -def test_unit_install_backs_up_changed_file( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - systemd_module.pwd, - "getpwuid", - lambda _uid: SimpleNamespace( - pw_name="operator", pw_dir="/srv/operators/operator" - ), - ) - monkeypatch.setattr( - systemd_module.grp, - "getgrgid", - lambda _gid: SimpleNamespace(gr_name="operator"), - ) - checkout = tmp_path / "checkout" - source = checkout / "docker/host-runtime/systemd" - source.mkdir(parents=True) - unit_root = tmp_path / "units" - unit_root.mkdir() - for name in ("akashic-host-bridge.service", "akashic-core.service"): - (source / name).write_text( - f"[Unit]\nDescription=new {name}\n" - "[Service]\nUser=huashen\nGroup=huashen\n", - encoding="utf-8", - ) - (unit_root / name).write_text(f"old {name}\n", encoding="utf-8") - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - assert install_units( - checkout=checkout, - backup_root=tmp_path / "backups", - run=run, - unit_root=unit_root, - ) - backup = next((tmp_path / "backups").iterdir()) - assert (backup / "akashic-core.service").read_text().startswith("old") - assert calls == [] - rendered = (unit_root / "akashic-core.service").read_text() - assert "Description=new" in rendered - assert "User=operator" in rendered - assert "Group=operator" in rendered - - -def test_unit_install_accepts_canonical_huashen_service_identity( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - systemd_module.pwd, - "getpwuid", - lambda _uid: SimpleNamespace(pw_name="huashen", pw_dir="/home/huashen"), - ) - monkeypatch.setattr( - systemd_module.grp, - "getgrgid", - lambda _gid: SimpleNamespace(gr_name="huashen"), - ) - repository = Path(__file__).resolve().parents[1] - unit_root = tmp_path / "units" - unit_root.mkdir() - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - assert install_units( - checkout=repository, - backup_root=tmp_path / "backups", - run=run, - unit_root=unit_root, - ) - - assert calls == [] - for name in ("akashic-host-bridge.service", "akashic-core.service"): - rendered = (unit_root / name).read_text(encoding="utf-8") - assert "User=huashen" in rendered - assert "Group=huashen" in rendered - assert "%h" not in rendered - assert ( - "EnvironmentFile=/home/huashen/.config/akashic-container/runtime.env" - in rendered - ) - - -def test_unit_install_enables_unchanged_system_units( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.setattr( - systemd_module.pwd, - "getpwuid", - lambda _uid: SimpleNamespace(pw_name="huashen", pw_dir="/home/huashen"), - ) - monkeypatch.setattr( - systemd_module.grp, - "getgrgid", - lambda _gid: SimpleNamespace(gr_name="huashen"), - ) - checkout = Path(__file__).resolve().parents[1] - unit_root = tmp_path / "units" - unit_root.mkdir() - monkeypatch.setattr(systemd_module, "_SYSTEM_UNIT_ROOT", unit_root) - for name in ("akashic-host-bridge.service", "akashic-core.service"): - source = checkout / "docker/host-runtime/systemd" / name - rendered = systemd_module._render_unit( - source, "huashen", "huashen", Path("/home/huashen") - ) - (unit_root / name).write_bytes(rendered) - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - assert not install_units( - checkout=checkout, - backup_root=tmp_path / "backups", - run=run, - unit_root=unit_root, - ) - assert calls == [ - [ - "sudo", - "systemctl", - "enable", - "akashic-host-bridge.service", - "akashic-core.service", - ] - ] - - -def test_isolated_unit_root_requires_and_verifies_external_contract( - tmp_path: Path, -) -> None: - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - return subprocess.CompletedProcess(arguments, 0) - - with pytest.raises(RuntimeError, match="缺少外围服务合同"): - verify_external_service_contract(run=run, unit_root=tmp_path) - - external = tmp_path / "akashic-home-services.service" - external.write_text("[Service]\nExecStart=/usr/bin/true\n", encoding="utf-8") - verify_external_service_contract(run=run, unit_root=tmp_path) - - assert calls == [["systemd-analyze", "verify", str(external)]] - - -def test_operator_entrypoint_is_atomic_and_backed_up(tmp_path: Path) -> None: - checkout = tmp_path / "checkout" - source = checkout / "scripts/akashic-release" - source.parent.mkdir(parents=True) - source.write_text("#!/bin/sh\necho new\n", encoding="utf-8") - target = tmp_path / "bin/akashic-release" - target.parent.mkdir() - target.write_text("#!/bin/sh\necho old\n", encoding="utf-8") - - assert install_operator_entrypoint( - checkout=checkout, - backup_root=tmp_path / "backups", - target=target, - ) - - assert target.read_text(encoding="utf-8") == source.read_text(encoding="utf-8") - assert target.stat().st_mode & 0o777 == 0o755 - backup = next((tmp_path / "backups").iterdir()) - assert (backup / "akashic-release").read_text().endswith("echo old\n") - - -def test_bootstrap_pins_resolved_main_before_running_python(tmp_path: Path) -> None: - _source, remote, _first, commit = _repository(tmp_path) - fake_bin = tmp_path / "bin" - fake_bin.mkdir() - python = fake_bin / "python3" - python.symlink_to("/bin/echo") - environment = { - "PATH": f"{fake_bin}:/usr/bin:/bin", - "AKASHIC_INSTALL_ORIGIN": str(remote), - } - - result = subprocess.run( - ["sh", "scripts/install-akashic.sh", "--yes"], - cwd=Path(__file__).resolve().parents[1], - env=environment, - check=True, - capture_output=True, - text=True, - ) - - invoked = result.stdout.split() - assert "--yes" in invoked - commit_index = invoked.index("--commit") - assert invoked[commit_index + 1] == commit - - -def test_bootstrap_cli_imports_without_site_packages() -> None: - repository = Path(__file__).resolve().parents[1] - subprocess.run( - [ - sys.executable, - "-I", - "-S", - str(repository / "scripts/akashic_release/cli.py"), - "--help", - ], - cwd=repository, - check=True, - capture_output=True, - text=True, - ) - - -def test_migration_command_is_plan_only_and_requires_integrity(tmp_path: Path) -> None: - manifest = tmp_path / "rehearsal.json" - manifest.write_text( - json.dumps( - { - "consistency": {"attempts": 1}, - "cleanup": {"exact_paths": [str(tmp_path / "candidate")]}, - "databases": [ - { - "source_integrity_check": "ok", - "target_integrity_check": "ok", - } - ], - } - ), - encoding="utf-8", - ) - - plan = migration_plan(manifest) - - assert plan["mode"] == "plan_only" - assert plan["automaticDataWrites"] is False - assert isinstance(plan["phases"], list) - assert len(plan["phases"]) == 7 - - -def test_environment_reader_rejects_duplicate_keys(tmp_path: Path) -> None: - environment = tmp_path / "runtime.env" - environment.write_text("A=1\nA=2\n", encoding="utf-8") - with pytest.raises(RuntimeError, match="重复"): - read_environment(environment) diff --git a/tests/test_app_server.py b/tests/test_app_server.py deleted file mode 100644 index ad56f31a9..000000000 --- a/tests/test_app_server.py +++ /dev/null @@ -1,90 +0,0 @@ -from pathlib import Path -from types import SimpleNamespace -from typing import cast - -import pytest - -from agent.config_models import Config -from bootstrap import app_server -from bootstrap.workspace_lock import WorkspaceInstanceLock - - -@pytest.mark.asyncio -async def test_stdio_runtime_clears_stale_admissions_only_after_lock( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - """确认 stdio runtime 以 workspace owner 身份清理遗留租约。""" - observed: dict[str, bool] = {} - - def fail_after_build_request(*args, **kwargs): - observed["clear"] = kwargs["clear_stale_session_admissions"] - raise RuntimeError("stop after owner routing check") - - monkeypatch.setattr(app_server, "build_core_runtime", fail_after_build_request) - - with pytest.raises(RuntimeError, match="owner routing"): - await app_server.run_stdio_app_server(cast(Config, object()), tmp_path) - - assert observed == {"clear": True} - lock = WorkspaceInstanceLock(tmp_path) - lock.acquire() - lock.release() - - -@pytest.mark.asyncio -async def test_stdio_runtime_binds_conversation_before_plugin_load( - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, -) -> None: - events: list[str] = [] - - class FakeCore: - def __init__(self) -> None: - self.loop = object() - self.event_bus = object() - self.session_manager = SimpleNamespace(control_store=object()) - - def bind_conversation_runtime(self, runtime: object) -> None: - assert isinstance(runtime, FakeRuntime) - events.append("bind") - - async def start(self) -> None: - events.append("start") - - async def stop(self) -> None: - events.append("stop") - - class FakeRuntime: - def __init__(self, _store: object, _execute: object) -> None: - events.append("runtime") - - async def shutdown(self) -> None: - events.append("runtime.stop") - - class FakeService: - def __init__( - self, _runtime: object, _manager: object, _workspace: Path - ) -> None: - events.append("service") - - async def shutdown(self) -> None: - events.append("service.stop") - - class FakeServer: - def __init__(self, _service: object, *, max_message_bytes: int) -> None: - assert max_message_bytes == 1024 - - async def run(self) -> None: - events.append("serve") - - core = FakeCore() - config = SimpleNamespace(app_server=SimpleNamespace(max_message_bytes=1024)) - monkeypatch.setattr(app_server, "build_core_runtime", lambda *_a, **_kw: core) - monkeypatch.setattr(app_server, "ConversationRuntime", FakeRuntime) - monkeypatch.setattr(app_server, "ControlService", FakeService) - monkeypatch.setattr(app_server, "StdioAppServer", FakeServer) - - await app_server.run_stdio_app_server(cast(Config, config), tmp_path) - - assert events[:5] == ["runtime", "bind", "start", "service", "serve"] diff --git a/tests/test_bootstrap_toolsets_p1.py b/tests/test_bootstrap_toolsets_p1.py deleted file mode 100644 index 41d017b42..000000000 --- a/tests/test_bootstrap_toolsets_p1.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations -from typing import Any, cast - -from pathlib import Path -from types import SimpleNamespace - -from agent.config_models import Config, WiringConfig -from agent.tools.registry import ToolRegistry -from bootstrap.toolsets.protocol import ( - ToolsetRegistrationResult, - build_registration_result, -) -from bootstrap.tools import build_registered_tools -from bus.event_bus import EventBus - - -def test_build_registered_tools_uses_toolset_providers(monkeypatch, tmp_path: Path): - calls: list[str] = [] - - class _MetaProvider: - def __init__(self, readonly_tools): - self._readonly_tools = readonly_tools - - def register(self, registry, deps): - calls.append("meta") - return ToolsetRegistrationResult(source_name="meta_common") - - class _McpProvider: - def register(self, registry, deps): - calls.append("mcp") - return ToolsetRegistrationResult( - source_name="mcp", - extras={}, - ) - - monkeypatch.setattr( - "bootstrap.tools.resolve_toolset_provider", - lambda name, readonly_tools=None: { - "meta_common": _MetaProvider(readonly_tools), - "mcp": _McpProvider(), - }[name], - ) - monkeypatch.setattr("bootstrap.tools.build_readonly_tools", lambda *_, **__: {}) - tools, push_tool = build_registered_tools( - config=Config( - system_prompt="s", - wiring=WiringConfig(toolsets=["meta_common"]), - ), - workspace=tmp_path, - http_resources=cast(Any, SimpleNamespace()), - bus=cast(Any, SimpleNamespace(chat_lane=None)), - runtime_snapshot_store=cast(Any, object()), - session_store=object(), - tools=ToolRegistry(), - event_publisher=EventBus(), - ) - - assert calls == ["meta"] - assert push_tool is not None - - -def test_build_registration_result_uses_public_registry_names(): - registry = SimpleNamespace( - get_registered_names=lambda: {"a", "b", "always"}, - get_always_on_names=lambda: {"always"}, - ) - - result = build_registration_result( - registry=cast(Any, registry), - source_name="demo", - before={"a"}, - ) - - assert result.tool_names == ["always", "b"] - assert result.always_on_names == ["always"] diff --git a/tests/test_bootstrap_wiring_p2.py b/tests/test_bootstrap_wiring_p2.py deleted file mode 100644 index 4c4f65ca1..000000000 --- a/tests/test_bootstrap_wiring_p2.py +++ /dev/null @@ -1,610 +0,0 @@ -from __future__ import annotations -from copy import deepcopy -from typing import Any, cast - -import json -import sys -from pathlib import Path -from types import SimpleNamespace - -import pytest - -from agent.config import Config, DEFAULT_SOCKET -from agent.config_models import Config as ConfigModel, WiringConfig -from agent.lifecycle.facade import TurnLifecycle -from agent.lifecycle.types import AfterStepCtx -from agent.looping.interrupt import ActiveTurnState -from agent.tools.registry import ToolRegistry -from bootstrap.tools import _build_loop_deps, build_registered_tools -from bootstrap.wiring import ( - wire_turn_lifecycle, - resolve_context_factory, - resolve_toolset_provider, -) -from bus.event_bus import EventBus -from session.store import SessionStore - - -def _toml_value(value): - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, str): - return json.dumps(value, ensure_ascii=False) - if isinstance(value, list): - return "[" + ", ".join(_toml_value(item) for item in value) + "]" - return str(value) - - -def _dump_toml(data: dict, prefix: tuple[str, ...] = ()) -> list[str]: - lines: list[str] = [] - scalar_lines: list[str] = [] - - for key, value in data.items(): - if isinstance(value, dict): - continue - if ( - isinstance(value, list) - and value - and all(isinstance(item, dict) for item in value) - ): - continue - scalar_lines.append(f"{key} = {_toml_value(value)}") - - if prefix: - lines.append(f"[{'.'.join(prefix)}]") - lines.extend(scalar_lines) - if scalar_lines: - lines.append("") - - for key, value in data.items(): - if isinstance(value, dict): - lines.extend(_dump_toml(value, prefix + (key,))) - elif ( - isinstance(value, list) - and value - and all(isinstance(item, dict) for item in value) - ): - for item in value: - lines.append(f"[[{'.'.join(prefix + (key,))}]]") - for item_key, item_value in item.items(): - lines.append(f"{item_key} = {_toml_value(item_value)}") - lines.append("") - return lines - - -def _write_toml(path: Path, payload: dict) -> None: - normalized = deepcopy(payload) - normalized.pop("llm", None) - path.write_text("\n".join(_dump_toml(normalized)).strip() + "\n", encoding="utf-8") - - -def _write_wiring_config(path: Path, wiring: object) -> None: - _write_toml( - path, - { - "agent": {"system_prompt": "s", "wiring": wiring}, - }, - ) - - -def test_config_load_reads_wiring_block(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - "wiring": { - "context": "default", - "toolsets": ["fixture", "mcp"], - }, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.wiring.context == "default" - assert cfg.wiring.toolsets == ["fixture", "mcp"] - - -def test_config_load_rejects_retired_memory_wiring(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_wiring_config(cfg_path, {"memory": "default"}) - - with pytest.raises(ValueError, match="removed configuration"): - Config.load(cfg_path, workspace=tmp_path) - - -@pytest.mark.parametrize("toolsets", ["fixture", [1, 2], ["fixture", ""]]) -def test_config_load_rejects_invalid_wiring_toolsets( - tmp_path: Path, - toolsets: object, -): - cfg_path = tmp_path / "config.toml" - _write_wiring_config(cfg_path, {"toolsets": toolsets}) - - with pytest.raises(ValueError, match="agent.wiring.toolsets 必须是字符串数组"): - Config.load(cfg_path, workspace=tmp_path) - - -def test_config_load_preserves_empty_wiring_toolsets(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_wiring_config(cfg_path, {"toolsets": []}) - - assert Config.load(cfg_path, workspace=tmp_path).wiring.toolsets == [] - - -def test_config_load_rejects_invalid_wiring_table(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_wiring_config(cfg_path, "invalid") - - with pytest.raises(ValueError, match="agent.wiring 必须是 TOML table"): - Config.load(cfg_path, workspace=tmp_path) - - -def test_config_load_ignores_legacy_memory_v2_enabled(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": {"system_prompt": "s"}, - "memory_v2": { - "enabled": True, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert not hasattr(cfg, "memory_v2") - assert not hasattr(cfg, "memory") - - -def test_config_load_rejects_retired_memory_table(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": {"system_prompt": "s"}, - "memory": { - "enabled": True, - "embedding": { - "model_ref": "embedding-a", - }, - "retrieval": { - "score_threshold": 0.99, - "thresholds": {"event": 0.99}, - }, - "hyde": {"enabled": True}, - }, - }, - ) - - with pytest.raises(ValueError, match=r"\[memory\].*普通模型插件"): - Config.load(cfg_path, workspace=tmp_path) - - -def test_config_load_reads_compaction_and_app_server(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - }, - "app_server": { - "listen": "/tmp/dev-akashic.sock", - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert not hasattr(cfg, "context_compaction") - assert cfg.app_server.listen == "/tmp/dev-akashic.sock" - - -def test_config_load_reads_agent_dev_mode(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - "dev_mode": True, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.dev_mode is True - - -def test_config_load_accepts_dev_model_alias(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - "dev_model": True, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.dev_mode is True - - -def test_config_load_skips_unfilled_channels(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - }, - "channels": { - "telegram": { - "token": "${TELEGRAM_BOT_TOKEN}", - "allow_from": ["user1"], - }, - "qq": { - "bot_uin": "", - "allow_from": ["42"], - }, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.channels.telegram is None - assert cfg.channels.qq is None - assert cfg.app_server.listen == DEFAULT_SOCKET - - -def test_config_load_reads_toml_layout(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - cfg_path.write_text( - """ -[agent] -system_prompt = "s" - -[app_server] -listen = "/tmp/toml-akashic.sock" - -""".strip() + "\n", - encoding="utf-8", - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.system_prompt == "s" - assert not hasattr(cfg, "context_compaction") - if sys.platform == "win32": - assert cfg.app_server.listen != "/tmp/toml-akashic.sock" - assert cfg.app_server.listen.startswith("127.0.0.1:") - else: - assert cfg.app_server.listen == "/tmp/toml-akashic.sock" - - -def test_config_rejects_legacy_cli_socket(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": {"provider": "openai", "main": {"model": "m", "api_key": "k"}}, - "agent": {"system_prompt": "s"}, - "channels": {"socket": "/tmp/legacy.sock"}, - }, - ) - with pytest.raises(ValueError, match="app_server"): - _ = Config.load(cfg_path, workspace=tmp_path) - - -def test_config_load_reads_qq_websocket_timeout(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - }, - "channels": { - "qq": { - "bot_uin": "10001", - "allow_from": ["42"], - "websocket_open_timeout_seconds": 9.5, - }, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.channels.qq is not None - assert cfg.channels.qq.websocket_open_timeout_seconds == 9.5 - - -def test_config_load_reads_web_chat_config(tmp_path: Path): - cfg_path = tmp_path / "config.toml" - _write_toml( - cfg_path, - { - "llm": { - "provider": "openai", - "main": { - "model": "m", - "api_key": "k", - }, - }, - "agent": { - "system_prompt": "s", - }, - "channels": { - "chat": { - "enabled": True, - "host": "127.0.0.2", - "port": 6324, - }, - }, - }, - ) - - cfg = Config.load(cfg_path, workspace=tmp_path) - - assert cfg.channels.chat.enabled is True - assert not hasattr(cfg.channels.chat, "channel_name") - assert not hasattr(cfg.channels.chat, "host") - assert not hasattr(cfg.channels.chat, "port") - - -def test_build_registered_tools_respects_toolset_order_and_subset( - monkeypatch, tmp_path: Path -): - calls: list[str] = [] - - class _ToolsetProvider: - def __init__(self, name: str) -> None: - self._name = name - - def register(self, registry, deps): - calls.append(self._name) - extras = {} - return SimpleNamespace(extras=extras) - - monkeypatch.setattr( - "bootstrap.tools.resolve_toolset_provider", - lambda name, readonly_tools=None: _ToolsetProvider(name), - ) - monkeypatch.setattr("bootstrap.tools.build_readonly_tools", lambda *_, **__: {}) - config = ConfigModel( - system_prompt="s", - wiring=WiringConfig(toolsets=["fixture", "mcp"]), - ) - build_registered_tools( - config=config, - workspace=tmp_path, - http_resources=cast(Any, SimpleNamespace()), - bus=cast(Any, SimpleNamespace(chat_lane=None)), - runtime_snapshot_store=cast(Any, object()), - session_store=object(), - tools=ToolRegistry(), - event_publisher=EventBus(), - ) - - assert calls == ["fixture", "mcp"] - - -def test_build_registered_tools_failure_preserves_external_session_store( - monkeypatch, - tmp_path: Path, -): - class _FailingToolsetProvider: - def register(self, registry, deps): - raise RuntimeError("toolset registration failed") - - monkeypatch.setattr( - "bootstrap.tools.resolve_toolset_provider", - lambda name, readonly_tools=None: _FailingToolsetProvider(), - ) - monkeypatch.setattr("bootstrap.tools.build_readonly_tools", lambda *_, **__: {}) - store = SessionStore(tmp_path / "sessions.db") - try: - config = ConfigModel( - system_prompt="s", - wiring=WiringConfig(toolsets=["fixture"]), - ) - with pytest.raises(RuntimeError, match="toolset registration failed"): - build_registered_tools( - config=config, - workspace=tmp_path, - http_resources=cast(Any, SimpleNamespace()), - bus=cast(Any, SimpleNamespace(chat_lane=None)), - runtime_snapshot_store=cast(Any, object()), - session_store=store, - tools=ToolRegistry(), - ) - assert store._closed is False - finally: - store.close() - - -def test_build_loop_deps_uses_context_factory(monkeypatch, tmp_path: Path): - observed: dict[str, object] = {} - fake_context = object() - monkeypatch.setattr( - "bootstrap.tools.resolve_context_factory", - lambda name: ( - lambda workspace: observed.update( - {"name": name, "workspace": workspace} - ) - or fake_context - ), - ) - - config = ConfigModel( - system_prompt="s", - wiring=WiringConfig(context="default"), - ) - deps = _build_loop_deps( - config=config, - workspace=tmp_path, - bus=cast(Any, SimpleNamespace(chat_lane=None)), - tools=ToolRegistry(), - session_manager=cast( - Any, - SimpleNamespace( - get_or_create=lambda key: None, - save_async=lambda session: None, - ), - ), - presence=cast(Any, None), - processing_state=cast(Any, SimpleNamespace()), - event_bus=EventBus(), - ) - - assert observed["name"] == "default" - assert observed["workspace"] == tmp_path - assert deps.context is fake_context - - -def test_wiring_error_messages_list_available_choices(): - try: - resolve_context_factory("bad") - except ValueError as exc: - assert "可选值" in str(exc) - assert "default" in str(exc) - else: - raise AssertionError("resolve_context_factory should fail for bad name") - - try: - resolve_toolset_provider("bad") - except ValueError as exc: - assert "可选值" in str(exc) - assert "meta_common" in str(exc) - else: - raise AssertionError("resolve_toolset_provider should fail for bad name") - - -@pytest.mark.asyncio -async def test_wire_turn_lifecycle_registers_afterstep_progress_handler(): - bus = EventBus() - states: dict[str, ActiveTurnState] = { - "telegram:1": ActiveTurnState(session_key="telegram:1") - } - wire_turn_lifecycle( - lifecycle=TurnLifecycle(bus), - active_turn_states=states, - ) - - await bus.emit( - AfterStepCtx( - session_key="telegram:1", - channel="telegram", - chat_id="1", - iteration=0, - context_tokens_estimate=0, - tools_called=("noop",), - partial_reply="部分回复", - tools_used_so_far=("a", "b"), - tool_chain_partial=({"text": "tool", "calls": []},), - partial_thinking="思考", - has_more=True, - ) - ) - - state = states["telegram:1"] - assert state.partial_reply == "部分回复" - assert state.partial_thinking == "思考" - assert state.tools_used == ["a", "b"] - assert state.tool_chain_partial == [{"text": "tool", "calls": []}] - - -def test_build_registered_tools_without_mcp_toolset_still_returns_empty_registry( - monkeypatch, tmp_path: Path -): - monkeypatch.setattr( - "bootstrap.tools.resolve_toolset_provider", - lambda name, readonly_tools=None: SimpleNamespace( - register=lambda registry, deps: SimpleNamespace(extras={}) - ), - ) - monkeypatch.setattr("bootstrap.tools.build_readonly_tools", lambda *_, **__: {}) - config = ConfigModel( - system_prompt="s", - wiring=WiringConfig(toolsets=["fixture"]), - ) - tools, push_tool = build_registered_tools( - config=config, - workspace=tmp_path, - http_resources=cast(Any, SimpleNamespace()), - bus=cast(Any, SimpleNamespace(chat_lane=None)), - runtime_snapshot_store=cast(Any, object()), - session_store=object(), - tools=ToolRegistry(), - event_publisher=EventBus(), - ) - - assert tools.get_registered_names() == set() - assert push_tool is not None diff --git a/tests/test_build_host_runtime_release.py b/tests/test_build_host_runtime_release.py deleted file mode 100644 index 816068e2e..000000000 --- a/tests/test_build_host_runtime_release.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import subprocess -from pathlib import Path - -import pytest - -from scripts.build_host_runtime_release import ( - _assert_release_paths_safe, - _resolve_commit, -) - - -def _commit(tmp_path: Path, name: str) -> tuple[Path, str]: - repository = tmp_path / "repository" - repository.mkdir() - subprocess.run(["git", "init", "-q"], cwd=repository, check=True) - subprocess.run( - ["git", "config", "user.email", "test@example.invalid"], - cwd=repository, - check=True, - ) - subprocess.run(["git", "config", "user.name", "Test"], cwd=repository, check=True) - target = repository / name - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text("credential material", encoding="utf-8") - subprocess.run(["git", "add", "."], cwd=repository, check=True) - subprocess.run(["git", "commit", "-qm", "fixture"], cwd=repository, check=True) - commit = subprocess.run( - ["git", "rev-parse", "HEAD"], - cwd=repository, - check=True, - capture_output=True, - text=True, - ).stdout.strip() - return repository, commit - - -@pytest.mark.parametrize( - "name", - ["config.toml", "auth.json", ".env", "config.toml.settings-deadbeef.bak"], -) -def test_release_rejects_tracked_runtime_credentials(tmp_path: Path, name: str) -> None: - repository, commit = _commit(tmp_path, name) - with pytest.raises(RuntimeError, match="禁止发布"): - _assert_release_paths_safe(repository, commit) - - -def test_release_accepts_example_configuration(tmp_path: Path) -> None: - repository, commit = _commit(tmp_path, "config.example.toml") - _assert_release_paths_safe(repository, commit) - - -def test_release_accepts_tracked_fixture_configuration(tmp_path: Path) -> None: - repository, commit = _commit(tmp_path, "benchmark/config.toml") - _assert_release_paths_safe(repository, commit) - - -def test_release_rejects_mutable_commit_reference(tmp_path: Path) -> None: - repository, _ = _commit(tmp_path, "config.example.toml") - with pytest.raises(RuntimeError, match="完整 40 位"): - _resolve_commit(repository, "HEAD") diff --git a/tests/test_builtin_akashic_call_skill.py b/tests/test_builtin_akashic_call_skill.py deleted file mode 100644 index 9302843df..000000000 --- a/tests/test_builtin_akashic_call_skill.py +++ /dev/null @@ -1,185 +0,0 @@ -import json -import queue -import socket -import subprocess -import sys -import threading -from pathlib import Path -from typing import Protocol, cast - -from agent.skills import SkillsLoader - - -REPO_ROOT = Path(__file__).parents[1] -SKILL_ROOT = REPO_ROOT / "skills" / "akashic-call" - - -class _FrameStream(Protocol): - def readline(self) -> bytes: ... - - def write(self, data: bytes, /) -> int: ... - - def flush(self) -> None: ... - - -def test_akashic_call_is_discoverable_builtin(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - - record = loader.load_skill_record("akashic-call") - - assert record is not None - assert record.source == "builtin" - assert record.available is True - assert record.always is False - assert record.when_to_use - for trigger in ( - "调用 akashic", - "程序化调用 Akashic", - "从 Codex 调用 Akashic", - "外部自动化调用", - "复用 Akashic session/thread", - ): - assert trigger in record.description - - -def test_akashic_call_content_preserves_runtime_boundaries(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - body = loader.load_skill_body("akashic-call") - - assert body is not None - for contract in ( - "固定模型、固定 workspace", - "`Thread` 是持久\nsession", - "禁止同步执行同 workspace", - "形成自死锁", - "不同 workspace、不同 runtime endpoint", - "禁止使用 `--last`", - "不会自动发送到 Telegram", - ): - assert contract in body - - -def test_akashic_call_examples_are_complete_and_referenced(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - body = loader.load_skill_body("akashic-call") - guide = (SKILL_ROOT / "references" / "external-caller.md").read_text(encoding="utf-8") - raw_client = SKILL_ROOT / "examples" / "raw_jsonrpc_uds.py" - - assert body is not None - assert "references/external-caller.md" in body - assert "examples/raw_jsonrpc_uds.py" in body - assert raw_client.is_file() - for command in ( - "exec \\", - '--thread "$AKASHIC_THREAD_ID"', - "Akashic.connect(endpoint)", - "thread_resume(os.environ[\"AKASHIC_THREAD_ID\"])", - '"method":"initialize"', - '"method":"thread/resume"', - '"method":"turn/start"', - ): - assert command in guide - assert "自动化不得用“最近一次会话”" in guide - assert "Akashic 首次 turn 执行失败" in guide - assert "Akashic JSONL 中缺少 threadId" in guide - assert 'printf \'%s\\n\' "$AKASHIC_THREAD_ID" > "$AKASHIC_THREAD_FILE"' in guide - assert "--timeout 600" in guide - compile(raw_client.read_text(encoding="utf-8"), str(raw_client), "exec") - - -def _read_frame(stream: _FrameStream) -> dict[str, object]: - payload = json.loads(stream.readline()) - if not isinstance(payload, dict): - raise ValueError("request frame must be an object") - return cast(dict[str, object], payload) - - -def _write_frame(stream: _FrameStream, payload: dict[str, object]) -> None: - _ = stream.write(json.dumps(payload, separators=(",", ":")).encode() + b"\n") - stream.flush() - - -def test_raw_client_buffers_terminal_arriving_before_turn_response(tmp_path: Path) -> None: - endpoint = tmp_path / "fake-akashic.sock" - ready = threading.Event() - failures: queue.SimpleQueue[BaseException] = queue.SimpleQueue() - - def serve() -> None: - """模拟在 turn/start response 前发出终态的合法服务端。""" - - try: - with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as listener: - listener.bind(str(endpoint)) - listener.listen(1) - ready.set() - connection, _ = listener.accept() - with connection, connection.makefile("rwb") as stream: - initialize = _read_frame(stream) - _write_frame(stream, {"jsonrpc": "2.0", "id": initialize["id"], "result": {}}) - assert _read_frame(stream)["method"] == "initialized" - - resume = _read_frame(stream) - _write_frame( - stream, - { - "jsonrpc": "2.0", - "id": resume["id"], - "result": {"id": "programmatic:test"}, - }, - ) - - start = _read_frame(stream) - _write_frame( - stream, - { - "jsonrpc": "2.0", - "method": "turn/completed", - "params": { - "threadId": "programmatic:test", - "turnId": "turn:fast", - "turn": { - "id": "turn:fast", - "status": "completed", - "finalResponse": "ok", - }, - }, - }, - ) - _write_frame( - stream, - { - "jsonrpc": "2.0", - "id": start["id"], - "result": {"id": "turn:fast"}, - }, - ) - except BaseException as exc: - failures.put(exc) - - server = threading.Thread(target=serve, daemon=True) - server.start() - assert ready.wait(timeout=2) - - completed = subprocess.run( - [ - sys.executable, - str(SKILL_ROOT / "examples" / "raw_jsonrpc_uds.py"), - str(endpoint), - "--thread", - "programmatic:test", - "--timeout", - "2", - "fast turn", - ], - capture_output=True, - text=True, - timeout=5, - check=False, - ) - server.join(timeout=2) - - if not failures.empty(): - raise failures.get() - assert not server.is_alive() - assert completed.returncode == 0, completed.stderr - assert '"method": "turn/completed"' in completed.stdout diff --git a/tests/test_builtin_develop_akashic_plugin_skill.py b/tests/test_builtin_develop_akashic_plugin_skill.py deleted file mode 100644 index cfd919758..000000000 --- a/tests/test_builtin_develop_akashic_plugin_skill.py +++ /dev/null @@ -1,290 +0,0 @@ -import json -import sqlite3 -import subprocess -import sys -from contextlib import closing -from pathlib import Path - -from agent.skills import SkillsLoader - -REPO_ROOT = Path(__file__).parents[1] -SKILL_ROOT = REPO_ROOT / "skills" / "develop-akashic-plugin" - - -def test_develop_akashic_plugin_is_discoverable_builtin(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - - record = loader.load_skill_record("develop-akashic-plugin") - - assert record is not None - assert record.source == "builtin" - assert record.available is True - assert record.always is False - for trigger in ( - "创建", - "编写", - "验证 Akashic v3 插件", - "插件内 Skill/MCP", - "递归自验证时使用", - ): - assert trigger in record.description - - -def test_develop_akashic_plugin_preserves_validation_contract(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - body = loader.load_skill_body("develop-akashic-plugin") - - assert body is not None - for contract in ( - "canonical source", - "不要直接编辑安装 cache", - "不要指定 runtime", - "attached programmatic child", - "只问“是否可见”", - "message_push", - "attached child", - "只有以下事实同时成立才报告完成", - ): - assert contract in body - - -def test_develop_akashic_plugin_references_are_complete() -> None: - body = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8") - authoring = (SKILL_ROOT / "references" / "plugin-authoring.md").read_text( - encoding="utf-8" - ) - validation = (SKILL_ROOT / "references" / "self-validation.md").read_text( - encoding="utf-8" - ) - diagnostics = (SKILL_ROOT / "references" / "runtime-diagnostics.md").read_text( - encoding="utf-8" - ) - - assert "references/plugin-authoring.md" in body - assert "references/self-validation.md" in body - assert "references/runtime-diagnostics.md" in body - for contract in ( - "akashic.plugin.toml", - "apply(ctx, config)", - "ServiceKey", - "TOOL_CATALOG", - "PluginToolDefinition", - "skill_roots", - "Context", - "CHANNELS", - ): - assert contract in authoring - for contract in ( - "成功只表示候选已准备", - "只有确有 Python 依赖时才声明", - "source test → commit/push → plugin-install", - ): - assert contract in body - for contract in ( - "plugin-install", - "stable、candidate generation、lease、排空、提交、恢复和 Channel 切换由 Core 管理", - "write_stdin", - "plugin-revert", - "port_env", - "semantic write set 为零", - "candidate 的写型 Tool/MCP", - "Channel 必须有 stop/start ownership 证据", - ): - assert contract in validation - for contract in ( - "mode=ro", - "items_json", - "llm_context_frame", - "tool_chain", - "runtime log unavailable: stderr is tty", - "plugin_prompt_probe", - "支持 `T await V → T 根据结果继续修改`", - ): - assert contract in diagnostics - - -def test_runtime_diagnostic_script_reads_turn_messages_and_reload( - tmp_path: Path, -) -> None: - workspace = tmp_path / "workspace" - runtime_dir = workspace / "runtime" - runtime_dir.mkdir(parents=True) - with closing(sqlite3.connect(workspace / "sessions.db")) as sessions: - sessions.executescript(""" - CREATE TABLE sessions ( - key TEXT PRIMARY KEY, - created_at TEXT NOT NULL, - updated_at TEXT NOT NULL, - last_consolidated INTEGER NOT NULL DEFAULT 0, - metadata TEXT, - next_seq INTEGER NOT NULL DEFAULT 0 - ); - CREATE TABLE turns ( - id TEXT PRIMARY KEY, - session_key TEXT NOT NULL, - status TEXT NOT NULL, - input_json TEXT NOT NULL, - items_json TEXT NOT NULL, - usage_json TEXT, - error_json TEXT, - final_response TEXT, - created_at TEXT NOT NULL, - started_at TEXT, - completed_at TEXT - ); - CREATE TABLE messages ( - id TEXT PRIMARY KEY, - session_key TEXT NOT NULL, - seq INTEGER NOT NULL, - role TEXT NOT NULL, - content TEXT, - tool_chain TEXT, - extra TEXT, - ts TEXT NOT NULL - ); - """) - sessions.execute( - "INSERT INTO sessions (key, created_at, updated_at, metadata) VALUES (?, ?, ?, ?)", - ( - "programmatic:probe", - "2026-08-06T00:00:00+00:00", - "2026-08-06T00:00:02+00:00", - '{"skip_post_memory":true}', - ), - ) - sessions.execute( - """ - INSERT INTO turns VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - """, - ( - "turn:probe", - "programmatic:probe", - "completed", - '{"input":"probe","metadata":{}}', - '[{"id":"item:1","type":"assistantMessage","data":{"content":"ok"}}]', - '{"inputTokens":1}', - None, - "ok", - "2026-08-06T00:00:00+00:00", - "2026-08-06T00:00:01+00:00", - "2026-08-06T00:00:02+00:00", - ), - ) - sessions.execute( - "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - ( - "message:1", - "programmatic:probe", - 0, - "user", - "probe", - None, - '{"llm_context_frame":"frame"}', - "2026-08-06T00:00:01+00:00", - ), - ) - sessions.commit() - with closing(sqlite3.connect(runtime_dir / "plugin-reloads.sqlite3")) as reloads: - reloads.executescript(""" - CREATE TABLE reload_transactions ( - tx_id TEXT PRIMARY KEY, - plugin_id TEXT NOT NULL, - phase TEXT NOT NULL, - started_at TEXT NOT NULL, - error TEXT NOT NULL - ); - CREATE TABLE reload_events ( - sequence INTEGER PRIMARY KEY, - tx_id TEXT NOT NULL, - phase TEXT NOT NULL, - details_json TEXT NOT NULL, - created_at TEXT NOT NULL - ); - """) - reloads.execute( - "INSERT INTO reload_transactions VALUES (?, ?, ?, ?, ?)", - ( - "tx:probe", - "probe@local", - "complete", - "2026-08-06T00:00:00+00:00", - "secret token", - ), - ) - reloads.execute( - "INSERT INTO reload_events VALUES (?, ?, ?, ?, ?)", - ( - 1, - "tx:probe", - "complete", - '{"snapshot":"latest"}', - "2026-08-06T00:00:02+00:00", - ), - ) - reloads.commit() - - completed = subprocess.run( - [ - sys.executable, - str(SKILL_ROOT / "scripts" / "inspect-runtime-trace.py"), - "--workspace", - str(workspace), - "--turn-id", - "turn:probe", - "--plugin-id", - "probe@local", - "--include-content", - ], - check=True, - capture_output=True, - text=True, - ) - - report = json.loads(completed.stdout) - assert report["turn"]["final_response"] == "ok" - assert report["turn"]["items"][0]["type"] == "assistantMessage" - assert report["messages"][0]["extra"]["llm_context_frame"] == "frame" - assert report["plugin_reload"]["phase"] == "complete" - assert report["plugin_reload"]["error"] == "secret token" - assert report["plugin_reload"]["events"][0]["details"] == {"snapshot": "latest"} - - redacted = subprocess.run( - [ - sys.executable, - str(SKILL_ROOT / "scripts" / "inspect-runtime-trace.py"), - "--workspace", - str(workspace), - "--turn-id", - "turn:probe", - "--plugin-id", - "probe@local", - ], - check=True, - capture_output=True, - text=True, - ) - summary = json.loads(redacted.stdout) - assert "final_response" not in summary["turn"] - assert summary["turn"]["final_response_summary"] == { - "chars": 4, - "type": "str", - } - assert "content" not in summary["messages"][0] - assert summary["messages"][0]["content_summary"] == { - "chars": 7, - "type": "str", - } - assert "details" not in summary["plugin_reload"]["events"][0] - assert "error" not in summary["plugin_reload"] - assert summary["plugin_reload"]["error_summary"] == { - "chars": 14, - "type": "str", - } - - -def test_plugin_system_routes_source_development_to_new_skill(tmp_path: Path) -> None: - loader = SkillsLoader(tmp_path, builtin_skills_dir=REPO_ROOT / "skills") - body = loader.load_skill_body("plugin-system") - - assert body is not None - assert "先加载 `develop-akashic-plugin`" in body diff --git a/tests/test_builtin_plugin_extraction.py b/tests/test_builtin_plugin_extraction.py deleted file mode 100644 index 0c0493105..000000000 --- a/tests/test_builtin_plugin_extraction.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -import importlib.util -import shutil -import sys -from pathlib import Path - -import pytest - - -ROOT = Path(__file__).resolve().parents[1] - - -@pytest.mark.parametrize( - ("source_name", "module_name", "plugin_name"), - ( - ("eventmail", "ordinary_eventmail", "eventmail"), - ("wake", "ordinary_wake", "wake"), - ), -) -def test_builtin_plugin_entrypoint_loads_from_an_external_directory( - tmp_path: Path, - source_name: str, - module_name: str, - plugin_name: str, -) -> None: - """Prove runtime imports resolve inside the copied plugin, not `plugins.*`.""" - - external_root = tmp_path / "installed" / source_name - shutil.copytree(ROOT / "plugins" / source_name, external_root) - spec = importlib.util.spec_from_file_location( - module_name, - external_root / "plugin.py", - submodule_search_locations=[str(external_root)], - ) - assert spec is not None and spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - try: - spec.loader.exec_module(module) - finally: - for imported in tuple(sys.modules): - if imported == module_name or imported.startswith(module_name + "."): - del sys.modules[imported] - - assert module.api_version == 3 - assert module.name == plugin_name diff --git a/tests/test_channel_base.py b/tests/test_channel_base.py deleted file mode 100644 index e2af3051f..000000000 --- a/tests/test_channel_base.py +++ /dev/null @@ -1,287 +0,0 @@ -import asyncio -from pathlib import Path - -import pytest - -from agent.control.context import running_turn_id -from infra.channels.base import AttachmentStore, MessageDeduper, SessionIdentityIndex -from session.manager import SessionManager -from session.store import SessionStore - - -def test_attachment_store_writes_under_configured_root(tmp_path: Path): - store = AttachmentStore(tmp_path / "uploads") - - path = store.write_bytes(b"hello", prefix="img_", suffix=".png") - - assert path.parent == tmp_path / "uploads" - assert path.suffix == ".png" - assert path.read_bytes() == b"hello" - - -def test_attachment_store_fails_when_root_is_not_a_directory(tmp_path: Path): - root = tmp_path / "not-a-directory" - root.write_text("occupied", encoding="utf-8") - store = AttachmentStore(root) - - with pytest.raises(FileExistsError): - store.write_bytes(b"hello", prefix="img_", suffix=".png") - - -def test_attachment_store_rejects_symlink_root(tmp_path: Path) -> None: - outside = tmp_path / "outside" - outside.mkdir() - root = tmp_path / "uploads" - root.symlink_to(outside, target_is_directory=True) - - with pytest.raises(ValueError, match="符号链接"): - AttachmentStore(root).write_bytes(b"hello", prefix="img_", suffix=".png") - - assert list(outside.iterdir()) == [] - - -def test_message_deduper_evicts_oldest_keys(): - deduper = MessageDeduper(max_size=2) - - assert deduper.seen("a") is False - assert deduper.seen("b") is False - assert deduper.seen("a") is True - assert deduper.seen("c") is False - assert deduper.seen("a") is False - - -@pytest.mark.asyncio -async def test_session_identity_index_rebuilds_and_persists_metadata(tmp_path: Path): - manager = SessionManager(tmp_path) - existing = manager.get_or_create("telegram:123") - existing.metadata["username"] = "alice" - manager.save(existing) - - index = SessionIdentityIndex( - manager, - channel="telegram", - metadata_key="username", - normalizer=lambda value: value.lower(), - ) - - rebuilt = index.rebuild() - assert rebuilt == {"alice": "123"} - assert index.resolve("ALICE") == "123" - - await index.remember("Bob", "456") - - assert index.mapping["bob"] == "456" - saved = manager.get_or_create("telegram:456") - assert saved.metadata["username"] == "bob" - token = running_turn_id.set("turn:identity-cache") - try: - grant = saved.issue_projection_grant("turn:identity-cache") - saved.revoke_projection_grant(grant) - finally: - running_turn_id.reset(token) - - -@pytest.mark.asyncio -async def test_session_identity_index_rolls_back_failed_metadata_save(tmp_path: Path): - manager = SessionManager(tmp_path) - - def fail_persist(**_kwargs: object) -> None: - raise OSError("metadata store unavailable") - - manager.control_store.persist_channel_identity = fail_persist # type: ignore[method-assign] - index = SessionIdentityIndex(manager, channel="telegram", metadata_key="username") - - with pytest.raises(OSError, match="metadata store unavailable"): - await index.remember("alice", "123") - - assert index.mapping == {} - assert manager.get_channel_identities("telegram") == {} - assert manager.control_store.get_session_meta("telegram:123") is None - assert "telegram:123" not in manager._cache - - -@pytest.mark.asyncio -async def test_session_identity_index_preserves_existing_session_on_failure( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - session = manager.get_or_create("telegram:123") - session.metadata["marker"] = "before" - manager.save(session) - - def fail_persist(**_kwargs: object) -> None: - raise OSError("metadata store unavailable") - - manager.control_store.persist_channel_identity = fail_persist # type: ignore[method-assign] - index = SessionIdentityIndex(manager, channel="telegram", metadata_key="username") - - with pytest.raises(OSError, match="metadata store unavailable"): - await index.remember("alice", "123") - - assert index.mapping == {} - assert session.metadata == {"marker": "before"} - assert manager.control_store.get_session_meta("telegram:123")["metadata"] == { - "marker": "before" - } - - -@pytest.mark.asyncio -async def test_session_identity_index_keeps_latest_owner_across_restart( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - manager.get_or_create("feishu:new") - index = SessionIdentityIndex( - manager, - channel="feishu", - metadata_key="feishu_open_id", - ) - - await index.remember("open-id", "old") - await index.remember("open-id", "new") - - assert manager.get_channel_identities("feishu") == {"open-id": "new"} - manager.close() - - reopened = SessionManager(tmp_path) - rebuilt = SessionIdentityIndex( - reopened, - channel="feishu", - metadata_key="feishu_open_id", - ) - - assert rebuilt.rebuild() == {"open-id": "new"} - assert rebuilt.resolve("open-id") == "new" - assert reopened.get_channel_identities("feishu") == {"open-id": "new"} - reopened.close() - - -@pytest.mark.asyncio -async def test_session_identity_index_concurrent_move_has_one_durable_owner( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - index = SessionIdentityIndex( - manager, - channel="feishu", - metadata_key="feishu_open_id", - ) - - await asyncio.gather( - index.remember("open-id", "first"), - index.remember("open-id", "second"), - ) - - durable = manager.get_channel_identities("feishu") - assert durable in ({"open-id": "first"}, {"open-id": "second"}) - assert index.mapping == durable - manager.close() - - -@pytest.mark.asyncio -async def test_session_identity_index_rolls_back_only_new_acceptance_state( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - index = SessionIdentityIndex( - manager, - channel="web", - metadata_key="web_identity", - ) - assert index.rebuild() == {} - assert manager.channel_identity_migration_completed("web") is True - - receipt = await index.remember("abc", "abc") - assert receipt is not None - assert await index.rollback(receipt) is True - - assert index.mapping == {} - assert manager.get_channel_identities("web") == {} - assert manager.control_store.get_session_meta("web:abc") is None - assert manager.channel_identity_migration_completed("web") is True - assert "web:abc" not in manager._cache - manager.close() - - -@pytest.mark.asyncio -async def test_session_identity_index_rollback_restores_existing_session( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - session = manager.get_or_create("web:abc") - session.metadata["marker"] = "before" - manager.save(session) - before = manager.control_store.get_session_meta("web:abc") - index = SessionIdentityIndex( - manager, - channel="web", - metadata_key="web_identity", - ) - - receipt = await index.remember("abc", "abc") - assert receipt is not None - assert await index.rollback(receipt) is True - - assert manager.control_store.get_session_meta("web:abc") == before - assert manager.get_channel_identities("web") == {} - assert index.mapping == {} - manager.close() - - -@pytest.mark.asyncio -async def test_session_identity_index_rollback_refuses_superseded_write( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - index = SessionIdentityIndex( - manager, - channel="web", - metadata_key="web_identity", - ) - - first = await index.remember("abc", "abc") - second = await index.remember("abc", "abc") - assert first is not None and second is not None - assert first.committed_updated_at != second.committed_updated_at - assert await index.rollback(first) is False - - assert manager.get_channel_identities("web") == {"abc": "abc"} - assert manager.control_store.get_session_meta("web:abc") is not None - assert index.mapping == {"abc": "abc"} - manager.close() - - -@pytest.mark.asyncio -async def test_session_delete_removes_identity_owner_and_backup_can_restore_it( - tmp_path: Path, -) -> None: - manager = SessionManager(tmp_path) - index = SessionIdentityIndex( - manager, - channel="feishu", - metadata_key="feishu_open_id", - ) - await index.remember("open-id", "old") - await index.remember("open-id", "new") - - audit = manager.delete_session_with_audit("feishu:new") - - assert audit.result == "committed" - assert manager.get_channel_identities("feishu") == {} - assert index.resolve("open-id") is None - assert audit.backup_path is not None - backup = SessionStore(audit.backup_path) - assert backup.get_channel_identities("feishu") == {"open-id": "new"} - backup.close() - manager.close() - - reopened = SessionManager(tmp_path) - rebuilt = SessionIdentityIndex( - reopened, - channel="feishu", - metadata_key="feishu_open_id", - ) - assert rebuilt.rebuild() == {} - assert reopened.channel_identity_migration_completed("feishu") is True - assert rebuilt.resolve("open-id") is None - reopened.close() diff --git a/tests/test_channel_clients.py b/tests/test_channel_clients.py deleted file mode 100644 index 1f2c76a4b..000000000 --- a/tests/test_channel_clients.py +++ /dev/null @@ -1,1350 +0,0 @@ -from __future__ import annotations - -import asyncio -import hashlib -import importlib -import logging -import sys -import types -from concurrent.futures import Future -from contextlib import asynccontextmanager -from pathlib import Path -from types import SimpleNamespace -from typing import Any, Mapping, cast -from unittest.mock import AsyncMock, MagicMock - -import httpx -import pytest - -from bus.event_bus import EventBus -from bus.events import OutboundMessage, channel_message_from_outbound -from bus.events_lifecycle import ( - StreamDeltaReady, - ToolCallCompleted, - ToolCallStarted, - TurnStarted, -) -from infra.channels.contract import ChannelContext -from agent.plugin_composition import ( - AttachmentKind, - AttachmentReadLease, - AttachmentRef, - ChannelFactoryContext, - CredentialRef, - ProviderClient, - RawInbound, -) -from agent.plugin_composition.channels import ChannelRuntimePorts - - -class _Bus: - def __init__(self) -> None: - self.inbound = [] - - async def publish_inbound(self, msg) -> None: - self.inbound.append(msg) - - -class _SessionManager: - def __init__(self, workspace: Path) -> None: - self.workspace = workspace - self.sessions = {} - self.saved = [] - self.channel_identities: dict[str, dict[str, str]] = {} - self.channel_identity_migrations: set[str] = set() - - def get_or_create(self, key: str): - return self.sessions.setdefault(key, SimpleNamespace(key=key, metadata={})) - - async def save_async(self, session) -> None: - self.saved.append(session.key) - - def get_channel_metadata(self, channel: str): - return [] - - def get_channel_identities(self, channel: str) -> dict[str, str]: - return dict(self.channel_identities.get(channel, {})) - - def channel_identity_migration_completed(self, channel: str) -> bool: - return channel in self.channel_identity_migrations - - def seed_channel_identities( - self, - channel: str, - mapping: dict[str, tuple[str, str]], - ) -> None: - self.channel_identities.setdefault( - channel, - {identity: chat_id for identity, (chat_id, _updated_at) in mapping.items()}, - ) - self.channel_identity_migrations.add(channel) - - async def remember_channel_identity( - self, - *, - channel: str, - identity: str, - chat_id: str, - metadata_key: str, - ) -> None: - session = self.get_or_create(f"{channel}:{chat_id}") - session.metadata[metadata_key] = identity - self.channel_identities.setdefault(channel, {})[identity] = chat_id - self.channel_identity_migrations.add(channel) - self.saved.append(session.key) - - -def _passive_channel_message(message: OutboundMessage): - """Project one committed legacy message into the v3 Channel adapter ABI.""" - - projected = channel_message_from_outbound(message) - projected.metadata["_channel_commit_role"] = "passive" - return projected - - -class _V3Ingress: - """Record only the frozen Core ingress objects accepted by a native channel.""" - - def __init__(self) -> None: - self.messages: list[RawInbound] = [] - - async def admit(self, raw: RawInbound) -> bool: - self.messages.append(raw) - return True - - -class _V3AttachmentImport: - """Return opaque attachment refs instead of a legacy temporary path.""" - - def __init__(self) -> None: - self.calls: list[tuple[bytes, AttachmentKind, str | None, str | None]] = [] - - async def import_bytes( - self, - data: bytes, - *, - kind: AttachmentKind, - filename: str | None, - media_type: str | None, - ) -> AttachmentRef: - self.calls.append((data, kind, filename, media_type)) - artifact_id = f"inbound-{len(self.calls)}" - return AttachmentRef( - artifact_id=artifact_id, - kind=kind, - filename=filename or artifact_id, - media_type=media_type or "application/octet-stream", - size_bytes=len(data), - sha256=hashlib.sha256(data).hexdigest(), - ) - - -class _UnusedProviderClient: - def credential(self, ref: CredentialRef) -> str: - raise AssertionError(f"unexpected credential lookup: {ref.path}") - - async def aclose(self) -> None: - return None - - -class _UnusedProviderFactory: - async def create( - self, - credentials: Mapping[str, CredentialRef], - ) -> ProviderClient: - return _UnusedProviderClient() - - async def aclose(self) -> None: - return None - - -class _NoAttachmentRead: - async def acquire(self, ref: AttachmentRef) -> AttachmentReadLease: - raise AssertionError("本测试不通过 native adapter 读取 outbound attachment") - - -async def _attach_native_v3_runtime(channel: object, *, binding_token: str): - """Attach one exact Core ingress and leave admission closed for the caller.""" - - ingress = _V3Ingress() - attachment_import = _V3AttachmentImport() - context = ChannelFactoryContext( - snapshot_id="test-snapshot", - generation_id="test-generation", - binding_token=binding_token, - config={}, - credentials={}, - provider_client_factory=_UnusedProviderFactory(), - ingress=ingress, - identity=None, - attachment_import=attachment_import, - attachment_read=_NoAttachmentRead(), - ) - adapter = channel.build_v3_adapter(context) - adapter.attach_runtime( - ChannelRuntimePorts( - snapshot_id=context.snapshot_id, - generation_id=context.generation_id, - binding_token=context.binding_token, - ingress=context.ingress, - identity=context.identity, - attachment_import=context.attachment_import, - ) - ) - assert (await adapter.start()).binding_token == binding_token - return adapter, ingress, attachment_import - - -def _import_telegram_channel(monkeypatch: pytest.MonkeyPatch): - telegram = types.ModuleType("telegram") - telegram_constants = types.ModuleType("telegram.constants") - telegram_error = types.ModuleType("telegram.error") - telegram_ext = types.ModuleType("telegram.ext") - - class Update: - ALL_TYPES = ["message"] - - class Bot: - async def edit_message_text(self, *args, **kwargs): - return True - - class BotCommand: - def __init__(self, command, description): - self.command = command - self.description = description - - class MessageEntity: - def __init__(self, *, type, offset, length): - self.type = type - self.offset = offset - self.length = length - - class TelegramError(Exception): - pass - - class Conflict(TelegramError): - pass - - class BadRequest(TelegramError): - pass - - class RetryAfter(TelegramError): - def __init__(self, retry_after=1.0): - super().__init__(retry_after) - self.retry_after = retry_after - - class NetworkError(TelegramError): - pass - - class TimedOut(TelegramError): - pass - - class _Filter: - def __and__(self, other): - return self - - def __invert__(self): - return self - - class _Document: - ALL = _Filter() - - class MessageHandler: - def __init__(self, flt, callback): - self.filter = flt - self.callback = callback - - class CommandHandler: - def __init__(self, command, callback): - self.command = command - self.callback = callback - - class _Updater: - def __init__(self): - self.running = False - self.error_callback = None - - async def start_polling(self, **kwargs): - self.running = True - self.error_callback = kwargs.get("error_callback") - - async def stop(self): - self.running = False - - class _Builder: - def __init__(self): - self._token = None - - def token(self, token): - self._token = token - return self - - def build(self): - return _Application(self._token) - - class _Application: - def __init__(self, token): - self.token = token - self.bot = SimpleNamespace( - send_message=AsyncMock(return_value=SimpleNamespace(message_id=99)), - edit_message_text=AsyncMock(), - send_document=AsyncMock(), - send_photo=AsyncMock(), - send_chat_action=AsyncMock(), - delete_message=AsyncMock(), - get_file=AsyncMock(), - set_my_commands=AsyncMock(), - ) - self.updater = _Updater() - self.handlers = [] - - @classmethod - def builder(cls): - return _Builder() - - async def initialize(self): - return None - - async def start(self): - return None - - async def stop(self): - return None - - async def shutdown(self): - return None - - def add_handler(self, handler): - self.handlers.append(handler) - - telegram.Bot = Bot - telegram.BotCommand = BotCommand - telegram.MessageEntity = MessageEntity - telegram.Update = Update - telegram_constants.ChatAction = SimpleNamespace(TYPING="typing") - telegram_error.Conflict = Conflict - telegram_error.BadRequest = BadRequest - telegram_error.NetworkError = NetworkError - telegram_error.RetryAfter = RetryAfter - telegram_error.TelegramError = TelegramError - telegram_error.TimedOut = TimedOut - telegram_ext.Application = _Application - telegram_ext.ContextTypes = SimpleNamespace(DEFAULT_TYPE=object) - telegram_ext.CommandHandler = CommandHandler - telegram_ext.MessageHandler = MessageHandler - telegram_ext.filters = SimpleNamespace( - TEXT=_Filter(), - COMMAND=_Filter(), - PHOTO=_Filter(), - Document=_Document(), - ) - monkeypatch.setitem(sys.modules, "telegram", telegram) - monkeypatch.setitem(sys.modules, "telegram.constants", telegram_constants) - monkeypatch.setitem(sys.modules, "telegram.error", telegram_error) - monkeypatch.setitem(sys.modules, "telegram.ext", telegram_ext) - sys.modules.pop("infra.channels.telegram_channel", None) - return importlib.import_module("infra.channels.telegram_channel") - - -def _import_qq_channel(monkeypatch: pytest.MonkeyPatch): - ncatbot_core = types.ModuleType("ncatbot.core") - ncatbot_core_adapter = types.ModuleType("ncatbot.core.adapter") - ncatbot_core_adapter_adapter = types.ModuleType("ncatbot.core.adapter.adapter") - ncatbot_utils = types.ModuleType("ncatbot.utils") - captured_connect_calls = [] - - class _Api: - def __init__(self): - self.calls = [] - - async def send_group_text(self, group_id, content): - self.calls.append(("group_text", group_id, content)) - - async def send_private_text(self, user_id, content): - self.calls.append(("private_text", user_id, content)) - - async def send_group_file(self, group_id, uri, name): - self.calls.append(("group_file", group_id, uri, name)) - - async def send_private_file(self, user_id, uri, name): - self.calls.append(("private_file", user_id, uri, name)) - - async def send_group_image(self, group_id, image): - self.calls.append(("group_image", group_id, image)) - - async def send_private_image(self, user_id, image): - self.calls.append(("private_image", user_id, image)) - - class BotClient: - def __init__(self): - self.api = _Api() - self.private_handler = None - self.group_handler = None - self.startup_handler = None - - def on_private_message(self): - def _wrap(fn): - self.private_handler = fn - return fn - - return _wrap - - def on_group_message(self): - def _wrap(fn): - self.group_handler = fn - return fn - - return _wrap - - def on_startup(self): - def _wrap(fn): - self.startup_handler = fn - return fn - - return _wrap - - def run_backend(self): - return self.api - - def exit(self): - return None - - class ForwardConstructor: - def __init__(self, user_id, nickname): - self.user_id = user_id - self.nickname = nickname - self.nodes = [] - - def attach_text(self, text, nickname=None): - self.nodes.append( - { - "type": "text", - "data": {"text": text}, - "nickname": nickname or self.nickname, - "user_id": self.user_id, - } - ) - - def to_forward(self): - class _Forward: - def __init__(self, nodes): - self._nodes = nodes - - def to_forward_dict(self): - return { - "messages": list(self._nodes), - "news": [], - "prompt": "", - "summary": "", - "source": "", - } - - return _Forward(self.nodes) - - def _fake_connect(*args, **kwargs): - captured_connect_calls.append(kwargs.copy()) - return ("connect", args, kwargs) - - ncatbot_core.BotClient = BotClient - ncatbot_core.ForwardConstructor = ForwardConstructor - ncatbot_core_adapter_adapter.websockets = SimpleNamespace(connect=_fake_connect) - ncatbot_core_adapter_adapter._captured_connect_calls = captured_connect_calls - ncatbot_utils.ncatbot_config = SimpleNamespace( - bt_uin="", - root="", - check_ncatbot_update=True, - skip_ncatbot_install_check=False, - napcat=SimpleNamespace(remote_mode=False, enable_webui=True), - enable_webui_interaction=True, - plugin=SimpleNamespace(plugins_dir=""), - ) - monkeypatch.setitem(sys.modules, "ncatbot.core", ncatbot_core) - monkeypatch.setitem(sys.modules, "ncatbot.core.adapter", ncatbot_core_adapter) - monkeypatch.setitem( - sys.modules, - "ncatbot.core.adapter.adapter", - ncatbot_core_adapter_adapter, - ) - monkeypatch.setitem(sys.modules, "ncatbot.utils", ncatbot_utils) - sys.modules.pop("infra.channels.qq_channel", None) - return importlib.import_module("infra.channels.qq_channel") - - -def test_qq_channel_ws_timeout_patch_is_best_effort( - monkeypatch: pytest.MonkeyPatch, -) -> None: - mod = _import_qq_channel(monkeypatch) - monkeypatch.delitem(sys.modules, "ncatbot.core.adapter.adapter", raising=False) - - mod._patch_ncatbot_ws_open_timeout(7.5) - - -@pytest.mark.asyncio -async def test_telegram_channel_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): - mod = _import_telegram_channel(monkeypatch) - bus = _Bus() - event_bus = EventBus() - session_manager = _SessionManager(tmp_path) - interrupt_controller = MagicMock() - interrupt_controller.request_interrupt.return_value = SimpleNamespace( - status="interrupted", - session_key="telegram:123", - message="已中断", - ) - channel = mod.TelegramChannel( - "token", - bus, - session_manager, - allow_from=["1", "Alice"], - command_catalog_provider=lambda: ( - ("memorystatus", "查看记忆整理状态"), - ("kvcache", "查看 KVCache 状态"), - ), - event_bus=event_bus, - interrupt_controller=interrupt_controller, - ) - channel._telegram_outbound_limiter = mod.TelegramOutboundLimiter( - send_interval_s=0.0, - edit_interval_s=0.0, - typing_interval_s=0.0, - global_interval_s=0.0, - retry_padding_s=0.0, - ) - channel._live_edit_queue = mod.TelegramLiveEditQueue( - min_interval_s=0.0, - limiter=channel._telegram_outbound_limiter, - ) - monkeypatch.setattr(mod, "send_markdown", AsyncMock()) - monkeypatch.setattr(mod, "send_stream_markdown", AsyncMock()) - monkeypatch.setattr(mod, "send_thinking_block", AsyncMock()) - await channel.start() - adapter, ingress, attachment_import = await _attach_native_v3_runtime( - channel, - binding_token="telegram-test-binding", - ) - assert len(channel._app.handlers) == 5 - assert [cmd.command for cmd in channel._app.bot.set_my_commands.await_args.args[0]] == [ - "memorystatus", - "kvcache", - "stop", - ] - await channel.replace_command_catalog((("status", "查看状态"),)) - assert [cmd.command for cmd in channel._app.bot.set_my_commands.await_args.args[0]] == [ - "status", - "stop", - ] - - class _File: - def __init__(self, payload: bytes): - self.payload = payload - - async def download_as_bytearray(self) -> bytearray: - return bytearray(self.payload) - - channel._app.bot.get_file = AsyncMock( - side_effect=[_File(b"reply-photo"), _File(b"reply-document"), _File(b"photo"), _File(b"reply-photo-2"), _File(b"document")] - ) - context = SimpleNamespace(bot=channel._app.bot) - reply_photo = [SimpleNamespace(file_id="p1")] - reply_doc = SimpleNamespace( - file_id="d1", - file_name="note.txt", - mime_type="text/plain", - ) - reply_user = SimpleNamespace(id=2, username="other") - reply_msg = SimpleNamespace( - text="原消息", - caption="", - photo=reply_photo, - document=reply_doc, - from_user=reply_user, - message_id=9, - ) - update = SimpleNamespace( - effective_message=SimpleNamespace( - text="你好", - message_id=1, - reply_to_message=reply_msg, - photo=None, - document=None, - ), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - pending_message = asyncio.create_task(channel._on_message(update, context)) - await asyncio.sleep(0) - assert ingress.messages == [] - adapter.open_admission() - await pending_message - assert len(ingress.messages) == 1 - first = ingress.messages[0].message - assert first.metadata["reply_to_sender"] == "@other" - assert len(first.attachments) == 2 - assert all(isinstance(ref, AttachmentRef) for ref in first.attachments) - - stop_update = SimpleNamespace( - effective_message=SimpleNamespace(text="/stop", message_id=99), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - await channel._on_stop_command(stop_update, context) - interrupt_controller.request_interrupt.assert_called_once_with( - session_key="telegram:123", - sender="1", - command="/stop", - ) - assert len(ingress.messages) == 1 - - status_update = SimpleNamespace( - effective_message=SimpleNamespace(text="/memorystatus", message_id=100), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - await channel._on_command(status_update, context) - assert len(ingress.messages) == 2 - assert ingress.messages[1].message.content == "/memorystatus" - assert ingress.messages[1].message.metadata["username"] == "Alice" - - kvcache_update = SimpleNamespace( - effective_message=SimpleNamespace(text="/kvcache 5", message_id=101), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - await channel._on_command(kvcache_update, context) - assert len(ingress.messages) == 3 - assert ingress.messages[2].message.content == "/kvcache 5" - assert ingress.messages[2].message.metadata["username"] == "Alice" - - photo_update = SimpleNamespace( - effective_message=SimpleNamespace( - photo=[SimpleNamespace(file_id="main"), SimpleNamespace(file_id="main2")], - message_id=2, - caption="图说", - reply_to_message=SimpleNamespace( - photo=[SimpleNamespace(file_id="rp")], - text="", - caption="", - from_user=reply_user, - message_id=10, - ), - ), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - await channel._on_photo(photo_update, context) - - doc_update = SimpleNamespace( - effective_message=SimpleNamespace( - document=SimpleNamespace(file_id="doc1", file_name="a.md", mime_type="text/plain"), - message_id=3, - caption="", - reply_to_message=None, - ), - effective_chat=SimpleNamespace(id=123), - effective_user=SimpleNamespace(id=1, username="Alice"), - ) - await channel._on_document(doc_update, context) - assert len(ingress.messages) == 5 - assert ingress.messages[-1].message.metadata["document_filename"] == "a.md" - assert len(attachment_import.calls) == 5 - assert bus.inbound == [] - - assert channel._resolve_chat_id("123") == "123" - await channel._identity_index.remember("alice", "456") - assert channel._resolve_chat_id("@Alice") == "456" - with pytest.raises(ValueError): - channel._resolve_chat_id("@missing") - - await channel.send("123", "hi") - await channel.send_stream("123", "stream hi") - sample = tmp_path / "doc.txt" - sample.write_text("x", encoding="utf-8") - await channel.send_file("123", str(sample), name="doc.txt", caption="cap") - await channel.send_image("123", "https://example.com/img.jpg") - await channel.send_image("123", str(sample)) - await channel._deliver_message(_passive_channel_message( - OutboundMessage(channel="telegram", chat_id="123", content="pong") - )) - assert mod.send_markdown.await_count == 3 - assert mod.send_stream_markdown.await_count == 1 - sender = channel.create_stream_sender("123") - assert sender is not None - await sender({"thinking_delta": "先想一点"}) - await sender("流式片段") - await sender("继续补充一大段内容继续补充一大段内容继续补充一大段内容继续补充一大段内容") - assert channel._app.bot.send_message.await_count >= 1 - before_send = channel._app.bot.send_message.await_count - before_edit = channel._app.bot.edit_message_text.await_count - live = mod.TelegramLiveTextMessage( - channel._app.bot, - mod.TelegramLiveEditQueue(min_interval_s=0.0), - 123, - ) - await asyncio.gather( - live.update("工具调用\na"), - live.update("工具调用\nb"), - live.update("工具调用\nc"), - ) - assert channel._app.bot.send_message.await_count == before_send + 1 - assert channel._app.bot.edit_message_text.await_count >= before_edit + 1 - await event_bus.observe( - StreamDeltaReady( - session_key="telegram:456", - channel="telegram", - chat_id="456", - content_delta="事件片段", - ) - ) - assert channel._active_streams.get("456") is None - await asyncio.sleep(0) - assert channel._live_messages.get("telegram:456") is not None - channel._thinking_live_next_at["telegram:456"] = 0.0 - await event_bus.observe( - StreamDeltaReady( - session_key="telegram:456", - channel="telegram", - chat_id="456", - thinking_delta="事件思考", - ) - ) - await asyncio.sleep(0) - live_texts = [ - call.kwargs.get("text", "") - for call in ( - channel._app.bot.send_message.await_args_list - + channel._app.bot.edit_message_text.await_args_list - ) - ] - assert any( - "临时回复" in text and "事件片段" in text and "思考过程" in text and "事件思考" in text - for text in live_texts - ) - assert any( - text.find("思考过程") < text.find("临时回复") - for text in live_texts - if "思考过程" in text and "临时回复" in text - ) - before_threshold_edit = channel._app.bot.edit_message_text.await_count - await event_bus.observe( - StreamDeltaReady( - session_key="telegram:456", - channel="telegram", - chat_id="456", - thinking_delta="继续分析" * 60, - ) - ) - await asyncio.sleep(0) - assert channel._app.bot.edit_message_text.await_count > before_threshold_edit - await event_bus.observe( - ToolCallStarted( - session_key="telegram:456", - channel="telegram", - chat_id="456", - iteration=1, - call_id="call-1", - tool_name="shell", - arguments={"cmd": "df -h", "description": "查看磁盘空间"}, - ) - ) - await event_bus.observe( - ToolCallCompleted( - session_key="telegram:456", - channel="telegram", - chat_id="456", - iteration=1, - call_id="call-1", - tool_name="shell", - arguments={"cmd": "df -h", "description": "查看磁盘空间"}, - final_arguments={"cmd": "df -h", "description": "查看磁盘空间"}, - status="ok", - result_preview="exit=0", - ) - ) - await asyncio.sleep(0) - if channel._live_tasks: - await asyncio.gather(*list(channel._live_tasks)) - assert channel._live_messages.get("telegram:456") is not None - assert any( - "工具调用" in call.kwargs.get("text", "") - for call in channel._app.bot.send_message.await_args_list - ) - tool_texts = [ - call.kwargs.get("text", "") - for call in ( - channel._app.bot.send_message.await_args_list - + channel._app.bot.edit_message_text.await_args_list - ) - if "工具调用" in call.kwargs.get("text", "") - ] - assert any( - "shell: 查看磁盘空间" in text and "df -h" in text and "✅" in text - for text in tool_texts - ) - assert all("exit=0" not in text for text in tool_texts) - long_text, long_html = mod._format_turn_live( - [ - mod._ToolLiveLine( - call_id="long", - tool_name="shell", - intent="查看长输出", - target="工具开头" + "x" * 1300 + "工具结尾", - status="done", - ) - ], - "回复开头" + "y" * 1300 + "回复结尾", - "思考开头" + "z" * 1600 + "思考结尾", - ) - assert "思考结尾" in long_text and "思考开头" not in long_text - assert "工具结尾" in long_text and "工具开头" not in long_text - assert "回复结尾" in long_text and "回复开头" not in long_text - assert "
    " in long_html and "
    " in long_html
    -    await channel._identity_index.remember("group", "-1001")
    -    assert channel.create_stream_sender("@group") is None
    -    await channel._deliver_message(_passive_channel_message(
    -        OutboundMessage(
    -            channel="telegram",
    -            chat_id="123",
    -            content="final",
    -            metadata={"streamed_reply": True},
    -        )
    -    ))
    -    assert channel._app.bot.edit_message_text.await_count >= 1
    -    sender = channel.create_stream_sender("123")
    -    assert sender is not None
    -    await sender({"thinking_delta": "分析中"})
    -    await channel._deliver_message(_passive_channel_message(
    -        OutboundMessage(
    -            channel="telegram",
    -            chat_id="123",
    -            content="final",
    -            thinking="分析中",
    -            metadata={"streamed_reply": True},
    -        )
    -    ))
    -    last_edit = channel._app.bot.edit_message_text.await_args_list[-1].kwargs["text"]
    -    assert last_edit == "final"
    -
    -    channel._app.bot.send_chat_action = AsyncMock(side_effect=[mod.TimedOut("x"), mod.NetworkError("x"), None])
    -    monkeypatch.setattr(mod.asyncio, "sleep", AsyncMock(return_value=None))
    -    await channel._safe_send_typing(context, 123)
    -    channel._app.bot.send_chat_action = AsyncMock(side_effect=RuntimeError("boom"))
    -    await channel._safe_send_typing(context, 123)
    -
    -    created = []
    -    real_create_task = asyncio.create_task
    -
    -    def _capture_task(coro):
    -        task = real_create_task(coro)
    -        created.append(task)
    -        return task
    -
    -    monkeypatch.setattr(mod.asyncio, "create_task", _capture_task)
    -    channel._on_polling_error(mod.Conflict("conflict"))
    -    if created:
    -        await asyncio.gather(*created)
    -    channel._on_polling_error(mod.TelegramError("warn"))
    -    adapter.close_admission()
    -    assert (await adapter.stop()).resources_closed is True
    -    await channel.stop()
    -
    -    merged, meta = mod._build_inbound_text_with_reply("hi", None)
    -    assert (merged, meta) == ("hi", {})
    -    merged, meta = mod._build_inbound_text_with_reply(
    -        "hi",
    -        SimpleNamespace(text="", caption="", photo=[1], from_user=None, message_id=11),
    -    )
    -    assert "[图片]" in merged
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_conflict_does_not_stop_updater(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    """核心回归:Conflict callback 不再干预 polling——不调用 updater.stop()、
    -    不创建任何恢复任务;退避重试完全交给 PTB 原生 network_retry_loop(max_retries=-1)。"""
    -    mod = _import_telegram_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    channel = mod.TelegramChannel("token", bus, session_manager)
    -    updater = channel._app.updater
    -    stop_calls = 0
    -
    -    async def _counting_stop():
    -        nonlocal stop_calls
    -        stop_calls += 1
    -        updater.running = False
    -
    -    monkeypatch.setattr(updater, "stop", _counting_stop)
    -
    -    channel._on_polling_error(mod.Conflict("conflict"))
    -
    -    assert stop_calls == 0  # 绝不停掉 PTB 自己的 retry loop
    -    assert channel._conflict_count == 1
    -    assert not hasattr(channel, "_polling_conflict_task")  # 不再有手动恢复任务
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_conflict_retry_loop_recovers(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog
    -):
    -    """模拟 PTB network_retry_loop 语义:一轮 getUpdates 409(触发 error_callback)后
    -    loop 继续运行,下一轮成功即恢复;callback 是纯观测,不改变 running、不调 stop。"""
    -    mod = _import_telegram_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    channel = mod.TelegramChannel("token", bus, session_manager)
    -    updater = channel._app.updater
    -    stop_calls = 0
    -
    -    async def _counting_stop():
    -        nonlocal stop_calls
    -        stop_calls += 1
    -        updater.running = False
    -
    -    monkeypatch.setattr(updater, "stop", _counting_stop)
    -
    -    # polling 运行中,第一轮 getUpdates 失败
    -    updater.running = True
    -    updater.error_callback = channel._on_polling_error
    -    with caplog.at_level(logging.WARNING, logger="infra.channels.telegram_channel"):
    -        updater.error_callback(mod.Conflict("conflict"))
    -
    -    # PTB loop 未被我们打断:running 保持 True,下一轮 getUpdates 可以继续
    -    assert updater.running is True
    -    assert stop_calls == 0
    -    assert channel._conflict_count == 1
    -    assert any("409 Conflict" in r.getMessage() for r in caplog.records)
    -
    -    # 第二轮成功:无错误回调,loop 保持运行 = 已恢复,无需人工干预
    -    updater.error_callback = None
    -    assert updater.running is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_conflict_stop_does_not_restart_polling(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    """热重载/关闭安全:运行中发生 Conflict 后 stop(),没有任何恢复任务会重新
    -    拉起 polling(旧实现会在 stop() 被 PTB 吞掉取消后重新 start_polling)。"""
    -    mod = _import_telegram_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    channel = mod.TelegramChannel("token", bus, session_manager)
    -    updater = channel._app.updater
    -    start_calls = 0
    -    stop_calls = 0
    -
    -    async def _counting_start(**kwargs):
    -        nonlocal start_calls
    -        start_calls += 1
    -        updater.running = True
    -        updater.error_callback = kwargs.get("error_callback")
    -
    -    async def _counting_stop():
    -        nonlocal stop_calls
    -        stop_calls += 1
    -        updater.running = False
    -
    -    monkeypatch.setattr(updater, "start_polling", _counting_start)
    -    monkeypatch.setattr(updater, "stop", _counting_stop)
    -
    -    updater.running = True
    -    updater.error_callback = channel._on_polling_error
    -    channel._on_polling_error(mod.Conflict("conflict"))  # 运行中冲突
    -    await channel.stop()  # 热重载/关闭
    -
    -    assert start_calls == 0  # 关键:stop 过程中没有任何恢复任务重新 start_polling
    -    assert stop_calls == 1  # stop() 正常停掉 updater
    -    assert channel._conflict_count == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_non_conflict_error_semantics_unchanged(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog
    -):
    -    """非 Conflict 错误语义保持不变:只打 warning,不进冲突观测分支。"""
    -    mod = _import_telegram_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    channel = mod.TelegramChannel("token", bus, session_manager)
    -
    -    with caplog.at_level(logging.WARNING, logger="infra.channels.telegram_channel"):
    -        channel._on_polling_error(mod.NetworkError("network down"))
    -        channel._on_polling_error(mod.TimedOut())
    -
    -    assert channel._conflict_count == 0  # 非 Conflict 不计入冲突
    -    assert channel._last_conflict_log_at is None  # 节流状态不变
    -    assert "polling 异常,框架将自动重试" in caplog.text
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_conflict_log_throttled(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path, caplog
    -):
    -    """日志节流:60s 窗口内多次 409 只打一条 warning,计数持续累计。"""
    -    mod = _import_telegram_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    channel = mod.TelegramChannel("token", bus, session_manager)
    -
    -    caplog.clear()
    -    with caplog.at_level(logging.WARNING, logger="infra.channels.telegram_channel"):
    -        channel._on_polling_error(mod.Conflict("conflict"))  # 首次
    -        channel._on_polling_error(mod.Conflict("conflict"))  # 立即重复:节流
    -        channel._on_polling_error(mod.Conflict("conflict"))  # 立即重复:节流
    -
    -    assert channel._conflict_count == 3
    -    assert len([r for r in caplog.records if "409 Conflict" in r.getMessage()]) == 1
    -
    -    # 把节流时间戳拨到 61s 前,模拟超过窗口:应再打一条
    -    channel._last_conflict_log_at -= 61
    -    caplog.clear()
    -    with caplog.at_level(logging.WARNING, logger="infra.channels.telegram_channel"):
    -        channel._on_polling_error(mod.Conflict("conflict"))
    -
    -    assert channel._conflict_count == 4
    -    assert len([r for r in caplog.records if "409 Conflict" in r.getMessage()]) == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_live_task_index_releases_finished_session(monkeypatch: pytest.MonkeyPatch):
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = object.__new__(mod.TelegramChannel)
    -    channel._live_tasks = set()
    -    channel._live_tasks_by_session = {}
    -
    -    async def complete() -> None:
    -        return None
    -
    -    channel._start_live_task("telegram:stale", complete())
    -    await asyncio.sleep(0)
    -    await asyncio.sleep(0)
    -
    -    assert channel._live_tasks == set()
    -    assert channel._live_tasks_by_session == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_live_message_is_retained_when_delete_fails(monkeypatch):
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = object.__new__(mod.TelegramChannel)
    -    message = SimpleNamespace(delete=AsyncMock(return_value=False))
    -    channel._live_messages = {"telegram:stale": message}
    -
    -    await channel._delete_live_message("telegram:stale")
    -
    -    assert channel._live_messages["telegram:stale"] is message
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_channel_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
    -    mod = _import_qq_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    class _Response:
    -        status_code = 200
    -        headers = {"content-type": "image/png"}
    -
    -        async def aiter_bytes(self, *, chunk_size: int):
    -            _ = chunk_size
    -            yield b"img"
    -
    -    class _Requester:
    -        @asynccontextmanager
    -        async def stream(self, method: str, url: str, **kwargs: object):
    -            _ = (method, kwargs)
    -            if not (url.endswith("a.jpg") or url.endswith("a.png")):
    -                raise RuntimeError("boom")
    -            yield _Response()
    -
    -    requester = _Requester()
    -    group_filter = SimpleNamespace(should_process=AsyncMock(return_value=True))
    -    group_cfg = SimpleNamespace(group_id="100")
    -    channel = mod.QQChannel(
    -        "42",
    -        bus,
    -        session_manager,
    -        allow_from=["1"],
    -        groups=[group_cfg],
    -        websocket_open_timeout_seconds=7.5,
    -        group_filter=group_filter,
    -        http_requester=requester,
    -        interrupt_controller=SimpleNamespace(
    -            request_interrupt=MagicMock(
    -                return_value=SimpleNamespace(
    -                    status="interrupted",
    -                    session_key="qq:1",
    -                    message="已中断",
    -                )
    -            )
    -        ),
    -    )
    -    adapter_mod = sys.modules["ncatbot.core.adapter.adapter"]
    -    adapter_mod.websockets.connect("ws://example.invalid", open_timeout=1)
    -    assert adapter_mod._captured_connect_calls[-1]["open_timeout"] == 7.5
    -    assert sys.modules["ncatbot.utils"].ncatbot_config.root == "1"
    -    assert channel._is_allowed("1") is True
    -    assert channel._is_allowed("2") is False
    -    assert mod._extract_cq_images("hello [CQ:image,url=http://x/a.jpg]") == ("hello", ["http://x/a.jpg"])
    -
    -    scheduled = []
    -    real_create_task = asyncio.create_task
    -
    -    def _run_coroutine_threadsafe(coro, loop):
    -        _ = loop
    -        if getattr(getattr(coro, "cr_code", None), "co_name", None) == "_execute_mock_call":
    -            coro.close()
    -            completed = Future()
    -            completed.set_result(True)
    -            return completed
    -        task = real_create_task(coro)
    -        scheduled.append(task)
    -        completed = Future()
    -
    -        def settle(result_task):
    -            if result_task.cancelled():
    -                completed.cancel()
    -                return
    -            try:
    -                completed.set_result(result_task.result())
    -            except BaseException as error:
    -                completed.set_exception(error)
    -
    -        task.add_done_callback(settle)
    -        return completed
    -
    -    monkeypatch.setattr(mod.asyncio, "run_coroutine_threadsafe", _run_coroutine_threadsafe)
    -    await channel.start()
    -    adapter, ingress, attachment_import = await _attach_native_v3_runtime(
    -        channel,
    -        binding_token="qq-test-binding",
    -    )
    -
    -    async def _drain(coro):
    -        return await coro
    -
    -    channel._run_on_bot_loop = AsyncMock(side_effect=_drain)
    -
    -    await channel._bot.startup_handler(SimpleNamespace())
    -    await channel._bot.private_handler(
    -        SimpleNamespace(
    -            user_id="1",
    -            raw_message="hi [CQ:image,url=http://x/a.jpg]",
    -            message_id="private-1",
    -        )
    -    )
    -    await asyncio.sleep(0)
    -    assert ingress.messages == []
    -    adapter.open_admission()
    -    await channel._bot.group_handler(
    -        SimpleNamespace(
    -            group_id="100",
    -            user_id="1",
    -            raw_message="hello",
    -            message_id="group-1",
    -        )
    -    )
    -    await channel._bot.private_handler(SimpleNamespace(user_id="1", raw_message="/stop"))
    -    await channel._bot.group_handler(SimpleNamespace(group_id="100", user_id="1", raw_message="/stop"))
    -    if scheduled:
    -        await asyncio.gather(*scheduled)
    -    assert len(ingress.messages) == 2
    -    assert ingress.messages[0].message.metadata["chat_type"] == "private"
    -    assert ingress.messages[1].message.metadata["chat_type"] == "group"
    -    assert ingress.messages[0].message.attachments[0].artifact_id == "inbound-1"
    -    assert attachment_import.calls[0][0] == b"img"
    -    assert bus.inbound == []
    -    assert channel._interrupt_controller.request_interrupt.call_count == 2
    -
    -    channel._run_on_bot_loop = AsyncMock(side_effect=_drain)
    -    sample = tmp_path / "image.bin"
    -    sample.write_bytes(b"abc")
    -    await channel.send("1", "pong")
    -    await channel.send("gqq:100", "group pong")
    -    await channel.send_file("1", str(sample), name="x.bin")
    -    await channel.send_image("1", str(sample))
    -    receipt = await channel._deliver_message(
    -        _passive_channel_message(
    -            OutboundMessage(channel="qq", chat_id="gqq:100", content="reply")
    -        )
    -    )
    -    assert receipt.succeeded
    -    assert channel._api.calls
    -    assert mod._is_local(str(sample)) is True
    -    assert mod._is_local("https://example.com/x.jpg") is False
    -    assert mod._local_to_base64(str(sample)).startswith("base64://")
    -    oversized = tmp_path / "oversized.bin"
    -    oversized.write_bytes(b"x" * (mod.MAX_QQ_IMAGE_BYTES + 1))
    -    with pytest.raises(ValueError, match="QQ 图片不能超过"):
    -        mod._local_to_base64(str(oversized))
    -
    -    channel._bot_loop = None
    -    pending = asyncio.sleep(0)
    -    with pytest.raises(RuntimeError):
    -        await mod.QQChannel._run_on_bot_loop(channel, pending)
    -    pending.close()
    -    adapter.close_admission()
    -    assert (await adapter.stop()).resources_closed is True
    -    await channel.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_private_trace_sends_forward_then_final(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -):
    -    mod = _import_qq_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    event_bus = EventBus()
    -    channel = mod.QQChannel(
    -        "42",
    -        bus,
    -        session_manager,
    -        allow_from=["1"],
    -        event_bus=event_bus,
    -        http_requester=SimpleNamespace(get=AsyncMock()),
    -    )
    -    await channel.start()
    -
    -    calls: list[tuple[str, object, object]] = []
    -
    -    async def _drain(coro):
    -        return await coro
    -
    -    async def _fake_send_private_forward_msg(user_id, **payload):
    -        calls.append(("forward", user_id, payload))
    -
    -    async def _fake_send_private_text(user_id, content):
    -        calls.append(("text", user_id, content))
    -
    -    async def _fake_get_login_info():
    -        return SimpleNamespace(user_id="42", nickname="Bot")
    -
    -    channel._run_on_bot_loop = AsyncMock(side_effect=_drain)
    -    channel._api.send_private_forward_msg = _fake_send_private_forward_msg
    -    channel._api.send_private_text = _fake_send_private_text
    -    channel._api.get_login_info = _fake_get_login_info
    -    channel._workspace = tmp_path
    -    (tmp_path / "memory").mkdir(parents=True, exist_ok=True)
    -    (tmp_path / "memory" / "SELF.md").write_text(
    -        "# Akashic 的自我认知\n- 我是 Steria,负责陪伴和协作。\n",
    -        encoding="utf-8",
    -    )
    -
    -    await event_bus.observe(
    -        TurnStarted(
    -            session_key="qq:1",
    -            channel="qq",
    -            chat_id="1",
    -            content="帮我看看最近的提交",
    -            timestamp=__import__("datetime").datetime.now(),
    -        )
    -    )
    -    await event_bus.observe(
    -        ToolCallStarted(
    -            session_key="qq:1",
    -            channel="qq",
    -            chat_id="1",
    -            iteration=1,
    -            call_id="call-1",
    -            tool_name="fetch_messages",
    -            arguments={"description": "查最近消息", "query": "最近提交"},
    -        )
    -    )
    -    await event_bus.observe(
    -        ToolCallCompleted(
    -            session_key="qq:1",
    -            channel="qq",
    -            chat_id="1",
    -            iteration=1,
    -            call_id="call-1",
    -            tool_name="fetch_messages",
    -            arguments={"description": "查最近消息", "query": "最近提交"},
    -            final_arguments={"description": "查最近消息", "query": "最近提交"},
    -            status="ok",
    -            result_preview='{"count": 21, "matched_count": 1}',
    -        )
    -    )
    -
    -    trace_message = OutboundMessage(
    -            channel="qq",
    -            chat_id="1",
    -            content="我看到了,最近主要是 QQ tracing 的改动。",
    -            thinking="先确认这轮是否有工具调用,再组织结论。",
    -        )
    -    await channel._send_private_trace("1", "qq:1", trace_message)
    -    receipt = await channel._deliver_message(_passive_channel_message(trace_message))
    -    assert receipt.succeeded
    -
    -    assert [item[0] for item in calls] == ["forward", "text"]
    -    forward_payload = cast(dict[str, Any], calls[0][2])
    -    assert forward_payload["news"] == [
    -        {"text": "Steria:【模型思路】"},
    -        {"text": "Steria:【工具链】"},
    -    ]
    -    assert "fetch_messages" in str(forward_payload)
    -    assert "命中 1 条,返回上下文 21 条" in str(forward_payload)
    -    assert calls[1] == ("text", 1, "我看到了,最近主要是 QQ tracing 的改动。")
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_private_trace_skips_empty_trace(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -):
    -    mod = _import_qq_channel(monkeypatch)
    -    bus = _Bus()
    -    session_manager = _SessionManager(tmp_path)
    -    event_bus = EventBus()
    -    channel = mod.QQChannel(
    -        "42",
    -        bus,
    -        session_manager,
    -        allow_from=["1"],
    -        event_bus=event_bus,
    -        http_requester=SimpleNamespace(get=AsyncMock()),
    -    )
    -    await channel.start()
    -
    -    calls: list[tuple[str, object, object]] = []
    -
    -    async def _drain(coro):
    -        return await coro
    -
    -    async def _fake_send_private_forward_msg(user_id, **payload):
    -        calls.append(("forward", user_id, payload))
    -
    -    async def _fake_send_private_text(user_id, content):
    -        calls.append(("text", user_id, content))
    -
    -    async def _fake_get_login_info():
    -        return SimpleNamespace(user_id="42", nickname="Bot")
    -
    -    channel._run_on_bot_loop = AsyncMock(side_effect=_drain)
    -    channel._api.send_private_forward_msg = _fake_send_private_forward_msg
    -    channel._api.send_private_text = _fake_send_private_text
    -    channel._api.get_login_info = _fake_get_login_info
    -
    -    await event_bus.observe(
    -        TurnStarted(
    -            session_key="qq:1",
    -            channel="qq",
    -            chat_id="1",
    -            content="好",
    -            timestamp=__import__("datetime").datetime.now(),
    -        )
    -    )
    -
    -    trace_message = OutboundMessage(
    -            channel="qq",
    -            chat_id="1",
    -            content="嗯,收到。",
    -            thinking=None,
    -        )
    -    await channel._send_private_trace("1", "qq:1", trace_message)
    -    receipt = await channel._deliver_message(_passive_channel_message(trace_message))
    -    assert receipt.succeeded
    -
    -    assert [item[0] for item in calls] == ["text"]
    -    assert calls[0] == ("text", 1, "嗯,收到。")
    diff --git a/tests/test_channel_host.py b/tests/test_channel_host.py
    deleted file mode 100644
    index 3c8129b3c..000000000
    --- a/tests/test_channel_host.py
    +++ /dev/null
    @@ -1,191 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from types import SimpleNamespace
    -
    -import pytest
    -
    -from agent.tools.message_push import MessagePushTool
    -from bootstrap.channel_host import ChannelHost
    -from bus.event_bus import EventBus
    -from bus.queue import MessageBus
    -
    -
    -class _Channel:
    -    def __init__(
    -        self,
    -        name: str,
    -        events: list[str],
    -        *,
    -        fail_start: bool = False,
    -        fail_stop: bool = False,
    -    ) -> None:
    -        self.name = name
    -        self._events = events
    -        self._fail_start = fail_start
    -        self._fail_stop = fail_stop
    -
    -    async def start(self, ctx: object) -> None:
    -        self._events.append(f"start:{self.name}:{ctx.log}")
    -        if self._fail_start:
    -            raise RuntimeError("start failed")
    -
    -    async def stop(self) -> None:
    -        self._events.append(f"stop:{self.name}")
    -        if self._fail_stop:
    -            raise RuntimeError("stop failed")
    -
    -
    -class _Event:
    -    pass
    -
    -
    -class _RegisteredChannel:
    -    def __init__(self, *, fail_start: bool = False) -> None:
    -        self.name = "registered"
    -        self._fail_start = fail_start
    -
    -    async def start(self, ctx: object) -> None:
    -        ctx.event_bus.on(_Event, lambda event: event)
    -        if self._fail_start:
    -            raise RuntimeError("registered start failed")
    -
    -    async def stop(self) -> None:
    -        return None
    -
    -
    -class _CommandCatalogChannel(_Channel):
    -    def __init__(self, events: list[str]) -> None:
    -        super().__init__("catalog", events)
    -        self.catalog: tuple[tuple[str, str], ...] = (("old", "old"),)
    -        self.fail_next = False
    -
    -    async def replace_command_catalog(
    -        self,
    -        commands: tuple[tuple[str, str], ...],
    -    ) -> None:
    -        self._events.append(f"commands:{commands[0][0] if commands else 'empty'}")
    -        self.catalog = commands
    -        if self.fail_next:
    -            self.fail_next = False
    -            raise RuntimeError("command publish failed")
    -
    -
    -def _context(channel: _Channel) -> SimpleNamespace:
    -    return SimpleNamespace(
    -        bus=SimpleNamespace(),
    -        session_manager=None,
    -        event_bus=SimpleNamespace(),
    -        push_tool=SimpleNamespace(),
    -        attachment_store=None,
    -        http_resources=None,
    -        interrupt_controller=None,
    -        command_catalog_provider=None,
    -        log=f"ctx:{channel.name}",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_host_start_failure_does_not_block_others():
    -    events: list[str] = []
    -    host = ChannelHost(_context)  # type: ignore[arg-type]
    -    host.add(_Channel("a", events))  # type: ignore[arg-type]
    -    host.add(_Channel("b", events, fail_start=True))  # type: ignore[arg-type]
    -    host.add(_Channel("c", events))  # type: ignore[arg-type]
    -
    -    with pytest.raises(RuntimeError, match="start failed"):
    -        await host.start_all()
    -
    -    assert events == [
    -        "start:a:ctx:a",
    -        "start:b:ctx:b",
    -        "stop:b",
    -        "start:c:ctx:c",
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_host_command_catalog_failure_restores_old_remote_state():
    -    events: list[str] = []
    -    channel = _CommandCatalogChannel(events)
    -    host = ChannelHost(_context)  # type: ignore[arg-type]
    -    host.add(channel)  # type: ignore[arg-type]
    -    await host.start_all()
    -    events.clear()
    -    channel.fail_next = True
    -
    -    with pytest.raises(RuntimeError, match="command publish failed"):
    -        await host.swap_command_catalog(
    -            (("old", "old"),),
    -            (("new", "new"),),
    -        )
    -
    -    assert channel.catalog == (("old", "old"),)
    -    assert events == ["commands:new", "commands:old"]
    -    await host.stop_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_host_stops_in_reverse_order():
    -    events: list[str] = []
    -    host = ChannelHost(_context)  # type: ignore[arg-type]
    -    host.add(_Channel("a", events))  # type: ignore[arg-type]
    -    host.add(_Channel("b", events, fail_stop=True))  # type: ignore[arg-type]
    -    host.add(_Channel("c", events))  # type: ignore[arg-type]
    -    await host.start_all()
    -    events.clear()
    -
    -    await host.stop_all()
    -
    -    assert events == ["stop:c", "stop:b", "stop:a"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_host_continues_after_cancelled_stop():
    -    events: list[str] = []
    -
    -    class _CancelledStopChannel(_Channel):
    -        async def stop(self) -> None:
    -            events.append(f"stop:{self.name}")
    -            raise asyncio.CancelledError
    -
    -    host = ChannelHost(_context)  # type: ignore[arg-type]
    -    host.add(_CancelledStopChannel("cancel", events))  # type: ignore[arg-type]
    -    host.add(_Channel("other", events))  # type: ignore[arg-type]
    -    await host.start_all()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await host.stop_all()
    -
    -    assert events[-2:] == ["stop:other", "stop:cancel"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_host_scopes_event_handlers_without_legacy_registrations():
    -    event_bus = EventBus()
    -    message_bus = MessageBus()
    -    push_tool = MessagePushTool()
    -    channel = _RegisteredChannel()
    -    context = SimpleNamespace(
    -        bus=message_bus,
    -        session_manager=None,
    -        event_bus=event_bus,
    -        push_tool=push_tool,
    -        attachment_store=None,
    -        http_resources=None,
    -        interrupt_controller=None,
    -        command_catalog_provider=None,
    -        log="ctx:registered",
    -    )
    -    host = ChannelHost(lambda _channel: context)  # type: ignore[arg-type]
    -    host.add(channel)  # type: ignore[arg-type]
    -
    -    await host.start_all()
    -
    -    assert event_bus.handler_count() == 1
    -    assert not hasattr(message_bus, "_subscribers")
    -    assert not hasattr(push_tool, "register_channel")
    -
    -    await host.stop_all()
    -
    -    assert event_bus.handler_count() == 0
    diff --git a/tests/test_clock.py b/tests/test_clock.py
    deleted file mode 100644
    index 4bc6ce6fb..000000000
    --- a/tests/test_clock.py
    +++ /dev/null
    @@ -1,52 +0,0 @@
    -from concurrent.futures import ThreadPoolExecutor
    -from datetime import UTC, datetime, timedelta
    -
    -import pytest
    -
    -from core.clock import ReplayClock, SystemClock, clock_from_env
    -
    -
    -def test_replay_clock_persists_and_advances(tmp_path) -> None:
    -    path = tmp_path / "clock.json"
    -    clock = ReplayClock(path, datetime(2026, 1, 2, 3, 4, tzinfo=UTC))
    -
    -    assert clock.now() == datetime(2026, 1, 2, 3, 4, tzinfo=UTC)
    -    assert clock.advance(timedelta(minutes=30)) == datetime(
    -        2026, 1, 2, 3, 34, tzinfo=UTC
    -    )
    -    assert ReplayClock(path).now() == datetime(2026, 1, 2, 3, 34, tzinfo=UTC)
    -
    -
    -def test_replay_clock_rejects_naive_datetime(tmp_path) -> None:
    -    with pytest.raises(ValueError, match="时区"):
    -        ReplayClock(tmp_path / "clock.json").set(datetime(2026, 1, 2))
    -
    -
    -def test_replay_clock_advance_is_atomic_within_instance(tmp_path) -> None:
    -    start = datetime(2026, 1, 2, tzinfo=UTC)
    -    clock = ReplayClock(tmp_path / "clock.json", start)
    -    worker_count = 8
    -    advances_per_worker = 50
    -
    -    def advance_many(_: int) -> list[datetime]:
    -        return [
    -            clock.advance(timedelta(minutes=1))
    -            for _ in range(advances_per_worker)
    -        ]
    -
    -    with ThreadPoolExecutor(max_workers=worker_count) as executor:
    -        batches = executor.map(advance_many, range(worker_count))
    -        results = [value for batch in batches for value in batch]
    -
    -    total_advances = worker_count * advances_per_worker
    -    assert len(set(results)) == total_advances
    -    assert clock.now() == start + timedelta(minutes=total_advances)
    -
    -
    -def test_clock_from_env_selects_replay_clock(tmp_path) -> None:
    -    path = tmp_path / "clock.json"
    -    ReplayClock(path, datetime(2026, 1, 2, tzinfo=UTC))
    -
    -    assert isinstance(clock_from_env({}), SystemClock)
    -    selected = clock_from_env({"AKASHIC_REPLAY_CLOCK_FILE": str(path)})
    -    assert selected.now() == datetime(2026, 1, 2, tzinfo=UTC)
    diff --git a/tests/test_codex_model_plugin.py b/tests/test_codex_model_plugin.py
    deleted file mode 100644
    index 098723ebc..000000000
    --- a/tests/test_codex_model_plugin.py
    +++ /dev/null
    @@ -1,700 +0,0 @@
    -from __future__ import annotations
    -
    -import ast
    -import asyncio
    -import base64
    -import json
    -import os
    -import shutil
    -import sqlite3
    -import subprocess
    -import sys
    -import threading
    -from contextlib import closing, contextmanager
    -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    -from pathlib import Path
    -from typing import Any, Iterator, Mapping
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    AddModel,
    -    CapabilitySources,
    -    CHAT_MODELS,
    -    FinishConnectionAuth,
    -    MODEL_CATALOG,
    -    MODEL_SETTINGS,
    -    ModelCapabilities,
    -    ModelContinuation,
    -    ModelKind,
    -    ModelAvailability,
    -    ModelRequest,
    -    ModelRole,
    -    ModelUnavailableError,
    -    SetDefaultModel,
    -    StartConnectionAuth,
    -    SyncModels,
    -    TransportError,
    -    UsageCoverage,
    -)
    -from agent.plugins.install import (
    -    finalize_uninstall_plugin,
    -    install_git_plugin,
    -    set_installed_plugin_enabled,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from bus.event_bus import EventBus
    -
    -
    -class _Server(ThreadingHTTPServer):
    -    daemon_threads = True
    -
    -    def __init__(self, address: tuple[str, int]) -> None:
    -        super().__init__(address, _Handler)
    -        self.requests: list[dict[str, Any]] = []
    -        self.refresh_count = 0
    -        self.response_count = 0
    -        self.poll_count = 0
    -        self.reject_barrier = threading.Barrier(2)
    -        self.use_reject_barrier = True
    -        self.refresh_status = 200
    -        self.catalog_default = "medium"
    -        self.catalog_efforts = ["medium", "high"]
    -
    -
    -class _Handler(BaseHTTPRequestHandler):
    -    server: _Server
    -
    -    def log_message(self, format: str, *args: object) -> None:
    -        _ = format, args
    -
    -    def do_GET(self) -> None:
    -        self.server.requests.append(
    -            {
    -                "method": "GET",
    -                "path": self.path,
    -                "authorization": self.headers.get("Authorization"),
    -            }
    -        )
    -        if self.path.startswith("/api/models"):
    -            self._json(
    -                200,
    -                {
    -                    "models": [
    -                        {
    -                            "slug": "gpt-codex-test",
    -                            "context_window": 120000,
    -                            "input_modalities": ["text", "image"],
    -                            "supported_reasoning_levels": [
    -                                {"effort": effort}
    -                                for effort in self.server.catalog_efforts
    -                            ],
    -                            "default_reasoning_level": self.server.catalog_default,
    -                            "supports_parallel_tool_calls": True,
    -                            "supports_reasoning_summary_parameter": True,
    -                        },
    -                        {
    -                            "slug": "hidden",
    -                            "context_window": 1,
    -                            "visibility": "hide",
    -                        },
    -                    ]
    -                },
    -            )
    -            return
    -        self._json(404, {"error": "missing"})
    -
    -    def do_POST(self) -> None:
    -        length = int(self.headers.get("Content-Length") or 0)
    -        raw = self.rfile.read(length) or b"{}"
    -        content_type = self.headers.get("Content-Type", "")
    -        body: object
    -        if "application/json" in content_type:
    -            body = json.loads(raw)
    -        else:
    -            body = raw.decode()
    -        self.server.requests.append(
    -            {
    -                "method": "POST",
    -                "path": self.path,
    -                "authorization": self.headers.get("Authorization"),
    -                "body": body,
    -            }
    -        )
    -        if self.path == "/auth/api/accounts/deviceauth/usercode":
    -            self._json(
    -                200,
    -                {"device_auth_id": "device-1", "user_code": "ABCD-EFGH", "interval": 1},
    -            )
    -            return
    -        if self.path == "/auth/api/accounts/deviceauth/token":
    -            self.server.poll_count += 1
    -            if self.server.poll_count == 1:
    -                self._json(403, {"status": "pending"})
    -                return
    -            self._json(
    -                200,
    -                {"authorization_code": "code-1", "code_verifier": "verifier-1"},
    -            )
    -            return
    -        if self.path == "/auth/oauth/token":
    -            if isinstance(body, Mapping) and body.get("grant_type") == "refresh_token":
    -                self.server.refresh_count += 1
    -                if self.server.refresh_status != 200:
    -                    self._json(
    -                        self.server.refresh_status,
    -                        {"error": {"message": "temporary token failure"}},
    -                    )
    -                    return
    -                self._json(
    -                    200,
    -                    {
    -                        "access_token": "access-new",
    -                        "refresh_token": "refresh-new",
    -                        "expires_in": 3600,
    -                    },
    -                )
    -            else:
    -                self._json(
    -                    200,
    -                    {
    -                        "access_token": "access-old",
    -                        "refresh_token": "refresh-old",
    -                        "id_token": _id_token("account-1"),
    -                        "expires_in": 3600,
    -                    },
    -                )
    -            return
    -        if self.path != "/api/responses":
    -            self._json(404, {"error": "missing"})
    -            return
    -        if self.headers.get("Authorization") == "Bearer access-old":
    -            if self.server.use_reject_barrier:
    -                self.server.reject_barrier.wait(timeout=2)
    -            self._json(401, {"error": {"message": "expired access-old"}})
    -            return
    -        self.server.response_count += 1
    -        assert isinstance(body, dict)
    -        turn = self.server.response_count
    -        truncate = _input_contains(body.get("input"), "truncate")
    -        done_truncate = _input_contains(body.get("input"), "done-truncate")
    -        invalid_tool = _input_contains(body.get("input"), "invalid-tool")
    -        mismatch_done = _input_contains(body.get("input"), "mismatch-done")
    -        self.send_response(200)
    -        self.send_header("Content-Type", "text/event-stream")
    -        if truncate:
    -            self.send_header("Content-Length", "10000")
    -        self.end_headers()
    -        events: list[dict[str, object]] = [
    -            {
    -                "type": "response.reasoning_summary_text.delta",
    -                "delta": f"think-{turn}",
    -            },
    -            {
    -                "type": "response.output_text.delta",
    -                "delta": f"answer-{turn}",
    -            },
    -            {
    -                "type": "response.output_item.done",
    -                "item": {
    -                    "type": "reasoning",
    -                    "summary": [{"type": "summary_text", "text": f"think-{turn}"}],
    -                    "encrypted_content": f"opaque-{turn}",
    -                    "id": "must-not-persist",
    -                },
    -            },
    -        ]
    -        if done_truncate:
    -            events = [{"type": "response.output_text.done", "text": "done-only"}]
    -        elif truncate:
    -            events = events[:1]
    -        if turn == 1 and not truncate:
    -            events.append(
    -                {
    -                    "type": "response.output_item.done",
    -                    "item": {
    -                        "type": "function_call",
    -                        "call_id": "call-1",
    -                        "name": "lookup",
    -                        "arguments": '{"id":7}',
    -                    },
    -                }
    -            )
    -        if invalid_tool:
    -            events.append(
    -                {
    -                    "type": "response.output_item.done",
    -                    "item": {
    -                        "type": "function_call",
    -                        "call_id": "invalid-call",
    -                        "name": "broken",
    -                        "arguments": "{",
    -                    },
    -                }
    -            )
    -        if mismatch_done:
    -            events.insert(
    -                1,
    -                {"type": "response.reasoning_summary_text.done", "text": "conflict"},
    -            )
    -        if not truncate:
    -            events.append(
    -                {
    -                    "type": "response.completed",
    -                    "response": {
    -                        "usage": {
    -                            "input_tokens": 10,
    -                            "output_tokens": 4,
    -                            "input_tokens_details": {"cached_tokens": 3},
    -                            "output_tokens_details": {"reasoning_tokens": 2},
    -                        }
    -                    },
    -                }
    -            )
    -        for event in events:
    -            self.wfile.write(f"data: {json.dumps(event)}\n\n".encode())
    -        self.wfile.flush()
    -        if truncate:
    -            self.close_connection = True
    -
    -    def _json(self, status: int, payload: object) -> None:
    -        encoded = json.dumps(payload).encode()
    -        self.send_response(status)
    -        self.send_header("Content-Type", "application/json")
    -        self.send_header("Content-Length", str(len(encoded)))
    -        self.end_headers()
    -        self.wfile.write(encoded)
    -
    -
    -def _id_token(account_id: str) -> str:
    -    payload = base64.urlsafe_b64encode(
    -        json.dumps(
    -            {"https://api.openai.com/auth": {"chatgpt_account_id": account_id}}
    -        ).encode()
    -    ).decode().rstrip("=")
    -    return f"header.{payload}.signature"
    -
    -
    -def _input_contains(raw: object, text: str) -> bool:
    -    return text in json.dumps(raw, ensure_ascii=False)
    -
    -
    -@contextmanager
    -def _provider() -> Iterator[tuple[_Server, str]]:
    -    server = _Server(("127.0.0.1", 0))
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    try:
    -        yield server, f"http://127.0.0.1:{server.server_port}"
    -    finally:
    -        server.shutdown()
    -        server.server_close()
    -        thread.join(timeout=2)
    -
    -
    -def _commit(path: Path) -> None:
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "initial"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args), cwd=path, capture_output=True, text=True, env=os.environ.copy()
    -        )
    -        assert result.returncode == 0, result.stderr
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_codex_is_an_ordinary_installed_plugin_with_login_refresh_and_continuation(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    source = Path("plugins/codex")
    -    for path in source.glob("*.py"):
    -        tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    -        for node in ast.walk(tree):
    -            if isinstance(node, ast.Import):
    -                assert all(not item.name.startswith("plugins.") for item in node.names)
    -            elif isinstance(node, ast.ImportFrom) and node.module:
    -                assert not node.module.startswith("plugins.")
    -                if node.module.startswith("agent."):
    -                    assert node.module == "agent.plugin_composition"
    -
    -    codex_repo = tmp_path / "codex-repo"
    -    models_repo = tmp_path / "models-repo"
    -    shutil.copytree(source, codex_repo)
    -    shutil.copytree(Path("plugins/models"), models_repo)
    -    shutil.rmtree(codex_repo / "__pycache__", ignore_errors=True)
    -    shutil.rmtree(models_repo / "__pycache__", ignore_errors=True)
    -    _commit(codex_repo)
    -    _commit(models_repo)
    -    models_install = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(models_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    codex_install = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(codex_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -
    -    blocked = ("plugins.models", "plugins.codex")
    -    for name in tuple(sys.modules):
    -        if name.startswith(blocked):
    -            monkeypatch.delitem(sys.modules, name)
    -
    -    class _BlockRepositoryPlugins:
    -        def find_spec(
    -            self,
    -            fullname: str,
    -            path: object = None,
    -            target: object = None,
    -        ) -> None:
    -            _ = path, target
    -            if fullname.startswith(blocked):
    -                raise ModuleNotFoundError(fullname)
    -            return None
    -
    -    monkeypatch.setattr(sys, "meta_path", [_BlockRepositoryPlugins(), *sys.meta_path])
    -    with _provider() as (server, endpoint):
    -        manager = _manager(tmp_path)
    -        await manager.load_all()
    -        for plugin_id, installed_path in (
    -            ("models@ordinary-test", models_install.installed_path),
    -            ("codex@ordinary-test", codex_install.installed_path),
    -        ):
    -            generation = manager.generation(plugin_id)
    -            assert generation is not None and generation.source_type == "installed"
    -            assert Path(generation.instance.module.__file__).resolve().is_relative_to(
    -                installed_path
    -            )
    -
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        root = snapshot.composition_root
    -        lease = await manager._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            settings = root.context.require(MODEL_SETTINGS)
    -            started = await settings.apply(
    -                StartConnectionAuth(
    -                    driver_id="codex",
    -                    connection_id="codex-main",
    -                    input={
    -                        "auth_base": f"{endpoint}/auth",
    -                        "api_base": f"{endpoint}/api",
    -                    },
    -                )
    -            )
    -            assert started.status == "pending" and started.attempt_id
    -            assert started.challenge == {
    -                "user_code": "ABCD-EFGH",
    -                "verification_uri": f"{endpoint}/auth/codex/device",
    -                "interval": 3,
    -            }
    -            finished = await settings.apply(
    -                FinishConnectionAuth(
    -                    expected_revision=0,
    -                    attempt_id=started.attempt_id,
    -                )
    -            )
    -            assert finished.status == "pending" and finished.revision == 0
    -            assert finished.challenge == started.challenge
    -            finished = await settings.apply(
    -                FinishConnectionAuth(
    -                    expected_revision=0,
    -                    attempt_id=started.attempt_id,
    -                )
    -            )
    -            assert finished.status == "committed" and finished.revision == 1
    -            registry_path = next((tmp_path / "workspace").rglob("model-registry.sqlite3"))
    -            with closing(sqlite3.connect(registry_path)) as connection:
    -                connection.execute(
    -                    "UPDATE model_connections SET catalog_provider_id = 'codex' "
    -                    "WHERE id = 'codex-main'"
    -                )
    -                connection.commit()
    -            revision = (
    -                await settings.apply(
    -                    SyncModels(
    -                        expected_revision=finished.revision,
    -                        connection_id="codex-main",
    -                    )
    -                )
    -            ).revision
    -            catalog = root.context.require(MODEL_CATALOG).snapshot()
    -            assert len(catalog.models) == 1
    -            model = catalog.models[0]
    -            assert model.model == "gpt-codex-test"
    -            assert model.capabilities.input_modalities == ("text", "image")
    -            server.catalog_default = "high"
    -            server.catalog_efforts = ["medium"]
    -            with pytest.raises(TransportError, match="不在支持列表"):
    -                await settings.apply(
    -                    SyncModels(
    -                        expected_revision=revision,
    -                        connection_id="codex-main",
    -                    )
    -                )
    -            assert root.context.require(MODEL_CATALOG).snapshot().revision == revision
    -            server.catalog_default = "medium"
    -            server.catalog_efforts = ["medium", "high"]
    -            with pytest.raises(ModelUnavailableError, match="does not provide embeddings"):
    -                await settings.apply(
    -                    AddModel(
    -                        expected_revision=revision,
    -                        model_id="forbidden-embedding",
    -                        connection_id="codex-main",
    -                        kind=ModelKind.EMBEDDING,
    -                        model="embed",
    -                        capabilities=ModelCapabilities(embedding_dimensions=3),
    -                        capability_sources=CapabilitySources(
    -                            embedding_dimensions="manual"
    -                        ),
    -                    )
    -                )
    -            revision = (
    -                await settings.apply(
    -                    SetDefaultModel(
    -                        expected_revision=revision,
    -                        role=ModelRole.DEFAULT,
    -                        model_id=model.model_id,
    -                    )
    -                )
    -            ).revision
    -            assert revision == 3
    -            async with root.context.require(CHAT_MODELS).execution() as execution:
    -                chat = execution.chat(ModelRole.DEFAULT)
    -                concurrent = await asyncio.gather(
    -                    chat.complete(ModelRequest(messages=({"role": "user", "content": "a"},))),
    -                    chat.complete(ModelRequest(messages=({"role": "user", "content": "b"},))),
    -                )
    -                assert {item.content for item in concurrent} == {"answer-1", "answer-2"}
    -                assert server.refresh_count == 1
    -                server.use_reject_barrier = False
    -                server.response_count = 0
    -                deltas: list[dict[str, str]] = []
    -
    -                async def on_delta(delta: dict[str, str]) -> None:
    -                    deltas.append(delta)
    -
    -                first = await chat.complete(
    -                    ModelRequest(
    -                        messages=({"role": "user", "content": "hello"},),
    -                        tools=({"type": "function", "function": {"name": "lookup"}},),
    -                        max_output_tokens=123,
    -                        on_delta=on_delta,
    -                    )
    -                )
    -                assert first.content == "answer-1"
    -                assert first.thinking == "think-1"
    -                assert first.tool_calls[0].arguments == {"id": 7}
    -                assert first.continuation is not None
    -                assert first.usage is not None
    -                assert first.usage.coverage is UsageCoverage.EXACT
    -                second = await chat.complete(
    -                    ModelRequest(
    -                        messages=(
    -                            {"role": "assistant", "tool_calls": [{
    -                                "id": "call-1",
    -                                "function": {"name": "lookup", "arguments": '{"id":7}'},
    -                            }]},
    -                            {"role": "tool", "tool_call_id": "call-1", "content": "found"},
    -                        ),
    -                        continuation=first.continuation,
    -                        disable_reasoning=True,
    -                    )
    -                )
    -                assert second.continuation is not None
    -                assert len(second.continuation.payload["items"]) == 2
    -                assert deltas == [
    -                    {"thinking_delta": "think-1"},
    -                    {"content_delta": "answer-1"},
    -                ]
    -                request_count = len(server.requests)
    -                with pytest.raises(ModelUnavailableError, match="binding"):
    -                    await chat.complete(
    -                        ModelRequest(
    -                            messages=(),
    -                            continuation=ModelContinuation(
    -                                binding_id="wrong-binding",
    -                                payload={"format_version": 1, "items": ()},
    -                            ),
    -                        )
    -                    )
    -                assert len(server.requests) == request_count
    -                interrupted_deltas: list[dict[str, str]] = []
    -
    -                async def collect(delta: dict[str, str]) -> None:
    -                    interrupted_deltas.append(delta)
    -
    -                with pytest.raises(TransportError, match="Codex Responses") as interrupted:
    -                    await chat.complete(
    -                        ModelRequest(
    -                            messages=({"role": "user", "content": "truncate"},),
    -                            on_delta=collect,
    -                        )
    -                    )
    -                assert interrupted_deltas
    -                assert interrupted.value.retryable is False
    -                done_deltas: list[dict[str, str]] = []
    -
    -                async def collect_done(delta: dict[str, str]) -> None:
    -                    done_deltas.append(delta)
    -
    -                with pytest.raises(TransportError) as done_interrupted:
    -                    await chat.complete(
    -                        ModelRequest(
    -                            messages=({"role": "user", "content": "done-truncate"},),
    -                            on_delta=collect_done,
    -                        )
    -                    )
    -                assert done_deltas == [{"content_delta": "done-only"}]
    -                assert done_interrupted.value.retryable is False
    -                with pytest.raises(TransportError, match="done text") as mismatch_done:
    -                    await chat.complete(
    -                        ModelRequest(
    -                            messages=({"role": "user", "content": "mismatch-done"},),
    -                            on_delta=lambda _delta: _async_none(),
    -                        )
    -                    )
    -                assert mismatch_done.value.retryable is False
    -                with pytest.raises(TransportError, match="arguments") as invalid_tool:
    -                    await chat.complete(
    -                        ModelRequest(
    -                            messages=({"role": "user", "content": "invalid-tool"},),
    -                            on_delta=lambda _delta: _async_none(),
    -                        )
    -                    )
    -                assert invalid_tool.value.retryable is False
    -
    -                with closing(sqlite3.connect(registry_path)) as connection:
    -                    encoded = connection.execute(
    -                        "SELECT auth_payload FROM model_connections WHERE id = 'codex-main'"
    -                    ).fetchone()
    -                    assert encoded is not None
    -                    credential = json.loads(encoded[0])
    -                    credential["access_token"] = "access-old"
    -                    connection.execute(
    -                        "UPDATE model_connections SET auth_payload = ? WHERE id = 'codex-main'",
    -                        (json.dumps(credential),),
    -                    )
    -                    connection.commit()
    -                revision_before_refresh_failure = (
    -                    root.context.require(MODEL_CATALOG).snapshot().revision
    -                )
    -                server.refresh_status = 503
    -                with pytest.raises(TransportError, match="token.*503"):
    -                    await chat.complete(ModelRequest(messages=()))
    -                assert (
    -                    root.context.require(MODEL_CATALOG).snapshot().revision
    -                    == revision_before_refresh_failure
    -                )
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -            await manager.terminate_all()
    -
    -        assert server.refresh_count == 2
    -        response_bodies = [
    -            item["body"] for item in server.requests if item["path"] == "/api/responses"
    -        ]
    -        first_body = next(
    -            body for body in response_bodies if body.get("max_output_tokens") == 123
    -        )
    -        continuation_body = next(
    -            body
    -            for body in response_bodies
    -            if body.get("input")
    -            and isinstance(body["input"][0], dict)
    -            and body["input"][0].get("encrypted_content") == "opaque-1"
    -        )
    -        assert first_body["reasoning"]["effort"] == "medium"
    -        assert "reasoning" not in continuation_body
    -        assert "id" not in continuation_body["input"][0]
    -        assert continuation_body["input"][1:] == [
    -            {
    -                "type": "function_call",
    -                "call_id": "call-1",
    -                "name": "lookup",
    -                "arguments": '{"id":7}',
    -            },
    -            {
    -                "type": "function_call_output",
    -                "call_id": "call-1",
    -                "output": "found",
    -            },
    -        ]
    -
    -        set_installed_plugin_enabled(
    -            "codex@ordinary-test",
    -            enabled=False,
    -            plugins_home=tmp_path / "home",
    -        )
    -        _ = finalize_uninstall_plugin(
    -            "codex@ordinary-test",
    -            workspace=tmp_path / "workspace",
    -            plugins_home=tmp_path / "home",
    -        )
    -        without = _manager(tmp_path)
    -        await without.load_all()
    -        snapshot = without.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.DRIVER_UNAVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG).snapshot().models
    -        )
    -        await without.terminate_all()
    -
    -        restored_install = install_git_plugin(
    -            workspace=tmp_path / "workspace",
    -            source=str(codex_repo),
    -            marketplace="ordinary-test",
    -            plugins_home=tmp_path / "home",
    -        )
    -        restored = _manager(tmp_path)
    -        await restored.load_all()
    -        generation = restored.generation("codex@ordinary-test")
    -        assert generation is not None and generation.plugin_dir == restored_install.installed_path
    -        snapshot = restored.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.AVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG).snapshot().models
    -        )
    -        server.refresh_status = 200
    -        restored_root = snapshot.composition_root
    -        restored_lease = await restored._snapshot_store.acquire()
    -        restored_token = bind_runtime_snapshot(restored_lease)
    -        try:
    -            async with restored_root.context.require(CHAT_MODELS).execution() as execution:
    -                result = await execution.chat(ModelRole.DEFAULT).complete(
    -                    ModelRequest(messages=({"role": "user", "content": "restored"},))
    -                )
    -                assert result.content is not None
    -        finally:
    -            reset_runtime_snapshot(restored_token)
    -            await restored_lease.release()
    -        await restored.terminate_all()
    -
    -    assert not any(name.startswith(blocked) for name in sys.modules)
    -
    -
    -async def _async_none() -> None:
    -    return None
    diff --git a/tests/test_compaction_markdown_memory_e2e.py b/tests/test_compaction_markdown_memory_e2e.py
    deleted file mode 100644
    index 3aabd5655..000000000
    --- a/tests/test_compaction_markdown_memory_e2e.py
    +++ /dev/null
    @@ -1,376 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -from datetime import UTC, datetime
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugins.manifest import builtin_plugin_data_dir
    -from agent.context import ContextBuilder
    -from agent.control.context import running_turn_id
    -from agent.core.passive_turn import DefaultReasoner
    -from agent.core.runtime_support import ToolDiscoveryState
    -from agent.looping.ports import LLMConfig
    -from agent.persona import reset_veda
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import (
    -    bind_runtime_snapshot,
    -    reset_runtime_snapshot,
    -)
    -from agent.plugin_composition import (
    -    ContextLengthError,
    -    LLMResponse,
    -    ModelRole,
    -    PROVIDER_REQUEST_PROJECTION,
    -    ProviderTurnInput,
    -    SessionCompactionStorage,
    -)
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from plugins.compaction import plugin as compaction_plugin
    -from plugins.compaction.engine import SUMMARY_HEADINGS
    -from plugins.compaction.receipts import SqliteCompactionReceipts
    -from plugins.compaction.runtime import _receipt_digest
    -from plugins.markdown_memory import plugin as markdown_plugin
    -from plugins.markdown_memory.store import MarkdownProfileStore
    -from plugins.markdown_memory.store import DEFAULT_SELF_MD
    -from session.manager import SessionManager
    -from tests.model_plugin_fakes import (
    -    BoundChatModelFake,
    -    register_test_model_provider,
    -    unregister_test_model_provider,
    -)
    -from tests.test_session_compaction_runtime import _seed_receipt
    -
    -
    -class _RecordedProvider:
    -    context_window = 5_000
    -    max_output_tokens = 1_024
    -    model = "fixture-model"
    -    runtime_id = "fixture-runtime"
    -
    -    def __init__(self) -> None:
    -        self.kinds: list[str] = []
    -        self.calls: list[list[dict[str, Any]]] = []
    -
    -    def estimate_context_tokens(
    -        self,
    -        messages: list[dict[str, Any]],
    -        tools: list[dict[str, Any]],
    -    ) -> int:
    -        return max(
    -            1,
    -            (
    -                sum(len(str(item.get("content", ""))) for item in messages)
    -                + len(json.dumps(tools))
    -            )
    -            // 4,
    -        )
    -
    -    def estimate_appended_message_tokens(self, messages: list[dict[str, Any]]) -> int:
    -        return self.estimate_context_tokens(messages, [])
    -
    -    async def chat(self, messages: list[dict[str, Any]], **_kwargs: Any) -> LLMResponse:
    -        self.calls.append([dict(message) for message in messages])
    -        rendered = json.dumps(messages, ensure_ascii=False)
    -        if "更新当前长任务的上下文压缩摘要" in rendered:
    -            self.kinds.append("summary")
    -            return LLMResponse(
    -                content="\n\n".join(f"{heading}\n- retained" for heading in SUMMARY_HEADINGS)
    -            )
    -        if "你维护两个长期 Markdown 档案" in rendered:
    -            self.kinds.append("markdown")
    -            memory = (
    -                "# 用户长期记忆\n\n"
    -                "## 用户事实\n- 已有事实必须保留\n- 花月长期使用 Akashic\n\n"
    -                "## 用户偏好\n- 喜欢简单设计\n\n"
    -                "## 用户明确要求长期记住的关键内容\n- 保持非特权插件边界\n"
    -            )
    -            return LLMResponse(
    -                content=json.dumps(
    -                    {"memory": memory, "self": DEFAULT_SELF_MD},
    -                    ensure_ascii=False,
    -                )
    -            )
    -        self.kinds.append("business")
    -        return LLMResponse(content="done")
    -
    -
    -class _OverflowProvider(_RecordedProvider):
    -    def __init__(self) -> None:
    -        super().__init__()
    -        self.calls: list[list[dict[str, Any]]] = []
    -
    -    async def chat(self, messages: list[dict[str, Any]], **_kwargs: Any) -> LLMResponse:
    -        self.calls.append([dict(message) for message in messages])
    -        raise ContextLengthError("provider context overflow")
    -
    -
    -@pytest.mark.asyncio
    -async def test_real_root_reasoner_updates_profiles_after_business_response(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    reset_veda(workspace)
    -    memory_path = workspace / "memory/MEMORY.md"
    -    memory_path.parent.mkdir(parents=True, exist_ok=True)
    -    memory_path.write_text(
    -        "# 用户长期记忆\n\n"
    -        "## 用户事实\n- 已有事实必须保留\n\n"
    -        "## 用户偏好\n\n"
    -        "## 用户明确要求长期记住的关键内容\n",
    -        encoding="utf-8",
    -    )
    -    provider = _RecordedProvider()
    -    register_test_model_provider(workspace, provider)
    -    sessions = SessionManager(workspace)
    -    session = sessions.get_or_create("web:e2e")
    -    for index in range(4):
    -        turn_id = f"old-{index}"
    -        session.add_message(
    -            "user",
    -            f"user-{index}-" + "u" * 900,
    -            control_turn_id=turn_id,
    -        )
    -        session.add_message(
    -            "assistant",
    -            f"assistant-{index}-" + "a" * 900,
    -            control_turn_id=turn_id,
    -        )
    -    sessions.save(session)
    -    message_count = len(session.messages)
    -    model_fixture = Path(__file__).parent / "fixtures/model_services"
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(compaction_plugin.__file__).parent,
    -            Path(markdown_plugin.__file__).parent,
    -            model_fixture,
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(),
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    compaction_data = builtin_plugin_data_dir("compaction", workspace)
    -    compaction_data.mkdir(parents=True, exist_ok=True)
    -    (compaction_data / "config.local.toml").write_text(
    -        "keep_recent_tokens = 1\n", encoding="utf-8"
    -    )
    -    await manager.load_all()
    -    lease = await manager.snapshot_store.acquire()
    -    snapshot_token = bind_runtime_snapshot(lease)
    -    turn_token = running_turn_id.set("turn:e2e")
    -    try:
    -        reasoner = DefaultReasoner(
    -            llm_config=LLMConfig(max_iterations=1, max_tokens=10),
    -            tools=ToolRegistry(),
    -            discovery=ToolDiscoveryState(),
    -            tool_search_enabled=False,
    -            context=ContextBuilder(workspace),
    -        )
    -        model = BoundChatModelFake(provider, role=ModelRole.AGENT)
    -        result = await reasoner.run_turn(
    -            msg=SimpleNamespace(
    -                content="continue",
    -                media=[],
    -                metadata={},
    -                channel="web",
    -                chat_id="e2e",
    -                timestamp=datetime.now(UTC),
    -            ),
    -            session=session,
    -            agent_model=model,
    -            fallback_model=model,
    -        )
    -    finally:
    -        running_turn_id.reset(turn_token)
    -        reset_runtime_snapshot(snapshot_token)
    -        await lease.release()
    -        await manager.terminate_all()
    -        sessions.close()
    -        unregister_test_model_provider(workspace)
    -
    -    assert result.reply == "done"
    -    assert provider.kinds == ["summary", "business", "markdown"]
    -    business_system = str(provider.calls[1][0]["content"])
    -    assert (
    -        business_system.index("## 行为规范")
    -        < business_system.index("## Akashic 自我认知")
    -        < business_system.index("## Long-term Memory")
    -        < business_system.index("## Current Session")
    -    )
    -    assert "花月长期使用 Akashic" in (
    -        workspace / "memory/MEMORY.md"
    -    ).read_text(encoding="utf-8")
    -    assert (workspace / "memory/SELF.md").read_text(encoding="utf-8") == DEFAULT_SELF_MD
    -    verifier = SessionManager(workspace)
    -    try:
    -        assert len(verifier.get_existing("web:e2e").messages) == message_count
    -    finally:
    -        verifier.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_disabled_compaction_real_root_passes_payload_once_without_writes(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    provider = _OverflowProvider()
    -    register_test_model_provider(workspace, provider)
    -    sessions = SessionManager(workspace)
    -    session = sessions.get_or_create("web:disabled")
    -    session.add_message("user", "old user", control_turn_id="old")
    -    session.add_message("assistant", "old assistant", control_turn_id="old")
    -    sessions.save(session)
    -    before_count = len(session.messages)
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(compaction_plugin.__file__).parent,
    -            Path(__file__).parent / "fixtures/model_services",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(),
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -        disabled_builtin_plugins=frozenset({"compaction"}),
    -    )
    -    await manager.load_all()
    -    lease = await manager.snapshot_store.acquire()
    -    snapshot_token = bind_runtime_snapshot(lease)
    -    turn_token = running_turn_id.set("turn:disabled")
    -    try:
    -        reasoner = DefaultReasoner(
    -            llm_config=LLMConfig(max_iterations=1, max_tokens=10),
    -            tools=ToolRegistry(),
    -            discovery=ToolDiscoveryState(),
    -            tool_search_enabled=False,
    -            context=cast(ContextBuilder, SimpleNamespace(
    -                render=lambda request, **_: SimpleNamespace(
    -                    messages=[
    -                        {"role": "system", "content": "root"},
    -                        *request.history,
    -                        {"role": "user", "content": request.current_message},
    -                    ]
    -                )
    -            )),
    -        )
    -        model = BoundChatModelFake(provider, role=ModelRole.AGENT)
    -        result = await reasoner.run_turn(
    -            msg=SimpleNamespace(
    -                content="current",
    -                media=[],
    -                metadata={},
    -                channel="web",
    -                chat_id="disabled",
    -                timestamp=datetime.now(UTC),
    -            ),
    -            session=session,
    -            agent_model=model,
    -            fallback_model=model,
    -        )
    -    finally:
    -        running_turn_id.reset(turn_token)
    -        reset_runtime_snapshot(snapshot_token)
    -        await lease.release()
    -        await manager.terminate_all()
    -        sessions.close()
    -        unregister_test_model_provider(workspace)
    -
    -    assert len(provider.calls) == 1
    -    assert result.reply == "上下文过长无法处理,请尝试新建对话。"
    -    assert provider.calls[0] == [
    -        {"role": "system", "content": "root"},
    -        {"role": "user", "content": "old user"},
    -        {"role": "assistant", "content": "old assistant"},
    -        {"role": "user", "content": "current"},
    -    ]
    -    verifier = SessionManager(workspace)
    -    try:
    -        assert len(verifier.get_existing("web:disabled").messages) == before_count
    -        assert verifier.control_store.get_compaction_head("web:disabled").parent_generation == 0
    -    finally:
    -        verifier.close()
    -    assert not (workspace / "memory/consolidation_writes.db").exists()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v2_receipt_recovers_through_real_markdown_plugin_once(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    manager_sessions: list[SessionManager] = []
    -
    -    def manager_factory(path: Path) -> SessionManager:
    -        manager = SessionManager(path)
    -        manager_sessions.append(manager)
    -        return manager
    -
    -    sessions, probe, source_ref = _seed_receipt(
    -        workspace,
    -        manager_factory,
    -        version=2,
    -    )
    -    receipts = SqliteCompactionReceipts(
    -        workspace / "memory/consolidation_writes.db"
    -    )
    -    receipt = probe.receipts[source_ref]
    -    draft = receipt["markdown_draft"]
    -    assert isinstance(draft, dict)
    -    draft["pending_items"] = "- [identity] 花月长期使用 Akashic"
    -    receipt["digest"] = _receipt_digest(receipt)
    -    _ = receipts.write(source_ref, receipt)
    -    profile_store = MarkdownProfileStore(
    -        workspace / "memory/MEMORY.md",
    -        workspace / "memory/SELF.md",
    -        workspace / "memory/markdown-profile-writes.db",
    -    )
    -    provider = _RecordedProvider()
    -    register_test_model_provider(workspace, provider)
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(compaction_plugin.__file__).parent,
    -            Path(markdown_plugin.__file__).parent,
    -            Path(__file__).parent / "fixtures/model_services",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(),
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    root = manager.current_snapshot
    -    assert root is not None and root.composition_root is not None
    -    session = sessions.get_existing("session")
    -    turn_token = running_turn_id.set("turn:v2")
    -    try:
    -        grant = session.issue_projection_grant("turn:v2")
    -        storage = SessionCompactionStorage(sessions).scope(grant)
    -        input = ProviderTurnInput(
    -            session_key=session.key,
    -            session_created_at=session.created_at.isoformat(),
    -            history_units=storage.history_units(session.key),
    -            access_grant=grant,
    -        )
    -        service = root.composition_root.context.require(PROVIDER_REQUEST_PROJECTION)
    -        _ = await service.open_turn(input)
    -        first_memory = profile_store.read_memory()
    -        _ = await service.open_turn(input)
    -        assert profile_store.read_memory() == first_memory
    -    finally:
    -        running_turn_id.reset(turn_token)
    -        await manager.terminate_all()
    -        unregister_test_model_provider(workspace)
    -        for item in manager_sessions:
    -            item.close()
    -
    -    assert provider.kinds == []
    -    assert "- [identity] 花月长期使用 Akashic" in (
    -        workspace / "memory/MEMORY.md"
    -    ).read_text(encoding="utf-8")
    -    assert profile_store.is_applied(source_ref)
    diff --git a/tests/test_compaction_plugin.py b/tests/test_compaction_plugin.py
    deleted file mode 100644
    index 620de1e43..000000000
    --- a/tests/test_compaction_plugin.py
    +++ /dev/null
    @@ -1,254 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import ast
    -import hashlib
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugin_composition import PROVIDER_REQUEST_PROJECTION
    -from agent.plugin_composition import ProviderTurnInput, SessionCompactionStorage
    -from agent.control.context import running_turn_id
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -from plugins.compaction import plugin as compaction_plugin
    -from session.manager import SessionManager
    -from plugins.compaction.receipts import SqliteCompactionReceipts
    -from agent.model_runtime.compaction_migration_v1 import (
    -    compaction_scope_id,
    -    compaction_source_ref,
    -)
    -
    -
    -@pytest.mark.asyncio
    -async def test_compaction_mounts_as_ordinary_service_with_exact_file(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(compaction_plugin.__file__).parent],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    generation = snapshot.generations["compaction"]
    -    plugin = generation.instance
    -    assert isinstance(plugin, ComposablePlugin)
    -    assert plugin.workspace_roots == ()
    -    assert plugin.workspace_files == ("memory/consolidation_writes.db",)
    -    assert (
    -        snapshot.composition_root.context.get(PROVIDER_REQUEST_PROJECTION) is not None
    -    )
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_compaction_candidate_copies_only_receipt_file(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    memory = workspace / "memory"
    -    memory.mkdir(parents=True)
    -    receipt = memory / "consolidation_writes.db"
    -    receipts = SqliteCompactionReceipts(receipt)
    -    receipts.write("source:1", {"version": 3})
    -    secret = memory / "SECRET.md"
    -    secret.write_text("must not project", encoding="utf-8")
    -    formal_digest = hashlib.sha256(receipt.read_bytes()).hexdigest()
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(compaction_plugin.__file__).parent],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -
    -    candidate = await manager.prepare_candidate("compaction")
    -
    -    assert candidate is not None and candidate.validation_workspace is not None
    -    assert not tuple(candidate.validation_workspace.rglob("SECRET.md"))
    -    assert hashlib.sha256(receipt.read_bytes()).hexdigest() == formal_digest
    -    await manager.discard_prepared("compaction")
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_compaction_candidate_invocation_cannot_touch_formal_session(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    session = sessions.get_or_create("session")
    -    session.add_message("user", "kept", control_turn_id="turn-1")
    -    session.add_message("assistant", "kept", control_turn_id="turn-1")
    -    sessions.save(session)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(compaction_plugin.__file__).parent],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    before_db = hashlib.sha256((workspace / "sessions.db").read_bytes()).hexdigest()
    -    candidate = await manager.prepare_candidate("compaction")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    root = candidate.runtime_snapshot.composition_root
    -    assert root is not None
    -    turn_token = running_turn_id.set("turn:candidate")
    -    try:
    -        grant = session.issue_projection_grant("turn:candidate")
    -        with pytest.raises(RuntimeError, match="candidate 验证期禁止"):
    -            _ = await root.context.require(PROVIDER_REQUEST_PROJECTION).open_turn(
    -                ProviderTurnInput(
    -                    session_key=session.key,
    -                    session_created_at=session.created_at.isoformat(),
    -                    history_units=(),
    -                    access_grant=grant,
    -                )
    -            )
    -    finally:
    -        running_turn_id.reset(turn_token)
    -    assert hashlib.sha256((workspace / "sessions.db").read_bytes()).hexdigest() == before_db
    -    await manager.discard_prepared("compaction")
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -def test_session_projection_grant_is_turn_and_session_scoped(tmp_path: Path) -> None:
    -    sessions = SessionManager(tmp_path)
    -    for key in ("session-a", "session-b"):
    -        session = sessions.get_or_create(key)
    -        session.add_message("user", key, control_turn_id=f"turn:{key}")
    -        session.add_message("assistant", key, control_turn_id=f"turn:{key}")
    -        sessions.save(session)
    -    session_a = sessions.get_existing("session-a")
    -    token = running_turn_id.set("turn:scope")
    -    try:
    -        grant = session_a.issue_projection_grant("turn:scope")
    -        storage = SessionCompactionStorage(sessions).scope(grant)
    -        assert storage.history_units("session-a")
    -        with pytest.raises(PermissionError, match="scope 不匹配"):
    -            _ = storage.history_units("session-b")
    -    finally:
    -        running_turn_id.reset(token)
    -    with pytest.raises(PermissionError, match="scope 不匹配"):
    -        _ = storage.history_units("session-a")
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_session_projection_grant_is_revoked_for_inherited_child_context(
    -    tmp_path: Path,
    -) -> None:
    -    sessions = SessionManager(tmp_path)
    -    session = sessions.get_or_create("session")
    -    session.add_message("user", "kept", control_turn_id="turn:kept")
    -    session.add_message("assistant", "kept", control_turn_id="turn:kept")
    -    sessions.save(session)
    -    token = running_turn_id.set("turn:lease")
    -    grant = session.issue_projection_grant("turn:lease")
    -    storage = SessionCompactionStorage(sessions).scope(grant)
    -    release = asyncio.Event()
    -
    -    async def delayed_read() -> None:
    -        await release.wait()
    -        with pytest.raises(PermissionError, match="scope 不匹配"):
    -            _ = storage.history_units(session.key)
    -
    -    child = asyncio.create_task(delayed_read())
    -    session.revoke_projection_grant(grant)
    -    running_turn_id.reset(token)
    -    release.set()
    -    await child
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_compaction_can_be_disabled_without_a_required_core_service(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(compaction_plugin.__file__).parent],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -        disabled_builtin_plugins=frozenset({"compaction"}),
    -    )
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is None
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -def test_compaction_plugin_does_not_import_privileged_runtime_owners() -> None:
    -    source = Path(compaction_plugin.__file__).read_text(encoding="utf-8")
    -    imported = {
    -        node.module
    -        for node in ast.walk(ast.parse(source))
    -        if isinstance(node, ast.ImportFrom) and node.module is not None
    -    }
    -    assert imported.isdisjoint(
    -        {
    -            "agent.plugins.manager",
    -            "agent.looping.core",
    -            "core.memory.markdown",
    -            "core.memory.runtime",
    -            "session.manager",
    -            "session.store",
    -        }
    -    )
    -    assert "MEMORY.md" not in source
    -    assert "SELF.md" not in source
    -    assert "PENDING.md" not in source
    -
    -
    -def test_core_and_session_do_not_import_concrete_compaction_plugin() -> None:
    -    root = Path(__file__).parents[1]
    -    paths = [
    -        root / "agent/core",
    -        root / "agent/looping",
    -        root / "agent/plugin_composition",
    -        root / "session/manager.py",
    -    ]
    -    sources = "\n".join(
    -        path.read_text(encoding="utf-8")
    -        for item in paths
    -        for path in ([item] if item.is_file() else item.rglob("*.py"))
    -    )
    -    assert "plugins.compaction" not in sources
    -    assert "ContextCompactionConfig" not in sources
    -
    -
    -def test_historical_compaction_identity_is_frozen_without_plugin_import() -> None:
    -    identity_module = (
    -        Path(__file__).parents[1] / "agent/model_runtime/compaction_migration_v1.py"
    -    ).read_text(encoding="utf-8")
    -    assert "plugins.compaction" not in identity_module
    -    scope = compaction_scope_id("session", "2026-08-31T00:00:00+00:00")
    -    assert scope == "session@0a59cea82a844ffa"
    -    assert (
    -        compaction_source_ref(scope, 1)
    -        == "context-compaction:session@0a59cea82a844ffa:1:823ea2ad93fa664f"
    -    )
    diff --git a/tests/test_computer_plugin.py b/tests/test_computer_plugin.py
    deleted file mode 100644
    index 3722525ba..000000000
    --- a/tests/test_computer_plugin.py
    +++ /dev/null
    @@ -1,454 +0,0 @@
    -from __future__ import annotations
    -
    -import base64
    -import json
    -import os
    -import socket
    -import subprocess
    -import threading
    -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    -from pathlib import Path
    -from types import MappingProxyType
    -
    -import pytest
    -from fastapi import FastAPI
    -from fastapi.testclient import TestClient
    -from websockets.sync.server import serve
    -from websockets.typing import Subprotocol
    -
    -from agent.plugin_composition import CompositionError, DashboardContext
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.static_manifest import load_static_plugin_manifest
    -from agent.tools.registry import ToolRegistry
    -from agent.workloads.model import (
    -    WorkloadEndpoint,
    -    WorkloadLease,
    -    WorkloadStartReceipt,
    -    WorkloadStopReceipt,
    -)
    -from bus.event_bus import EventBus
    -from plugins.computer.dashboard import register as register_computer_dashboard
    -from plugins.computer.mcp_server import save_screenshot
    -
    -ROOT = Path(__file__).resolve().parents[1]
    -PLUGIN = ROOT / "plugins" / "computer"
    -_PNG = base64.b64decode(
    -    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
    -)
    -
    -
    -class _Gateway(BaseHTTPRequestHandler):
    -    def do_GET(self) -> None:  # noqa: N802
    -        if self.path == "/health":
    -            self._send("application/json", b'{"status":"ready"}')
    -            return
    -        if self.path == "/activity":
    -            self._send(
    -                "application/json",
    -                b'{"revision":2,"noticeId":1,"active":false}',
    -            )
    -            return
    -        if self.path.startswith("/screenshot"):
    -            self._send("image/png", _PNG)
    -            return
    -        self.send_error(404)
    -
    -    def do_POST(self) -> None:  # noqa: N802
    -        size = int(self.headers.get("content-length", "0"))
    -        payload = json.loads(self.rfile.read(size))
    -        if self.path == "/input":
    -            self._send("application/json", json.dumps(payload).encode())
    -            return
    -        if self.path == "/browser/observe":
    -            value = (
    -                {
    -                    "mimeType": "image/png",
    -                    "data": base64.b64encode(_PNG).decode("ascii"),
    -                }
    -                if payload["observe"] == "screenshot"
    -                else {"url": "https://example.com", "title": "Example Domain"}
    -            )
    -            self._send("application/json", json.dumps(value).encode())
    -            return
    -        if self.path == "/browser/action":
    -            self._send("application/json", json.dumps({"ok": True, **payload}).encode())
    -            return
    -        self.send_error(404)
    -
    -    def _send(self, media_type: str, body: bytes) -> None:
    -        self.send_response(200)
    -        self.send_header("content-type", media_type)
    -        self.send_header("content-length", str(len(body)))
    -        self.end_headers()
    -        self.wfile.write(body)
    -
    -    def log_message(self, _format: str, *args: object) -> None:
    -        _ = args
    -
    -
    -class _Controller:
    -    def __init__(self, endpoint: str) -> None:
    -        self.endpoint = endpoint
    -        self.starts = []
    -        self.stops = []
    -
    -    async def start(self, request) -> WorkloadStartReceipt:
    -        self.starts.append(request)
    -        lease = WorkloadLease(
    -            workspace_id=request.workspace_id,
    -            plugin_id=request.plugin_id,
    -            workload=request.workload,
    -            mode=request.mode,
    -            transaction_id=request.transaction_id,
    -            generation_id=request.generation_id,
    -            container_id=f"computer-{len(self.starts)}",
    -            spec_digest=request.spec_digest,
    -        )
    -        return WorkloadStartReceipt(
    -            lease,
    -            tuple(
    -                WorkloadEndpoint(name, self.endpoint) for name, _number in request.ports
    -            ),
    -            None,
    -        )
    -
    -    async def stop(self, lease: WorkloadLease) -> WorkloadStopReceipt:
    -        self.stops.append(lease)
    -        return WorkloadStopReceipt(lease, True, True)
    -
    -    async def cleanup_candidates(
    -        self, workspace_id: str
    -    ) -> tuple[WorkloadStopReceipt, ...]:
    -        _ = workspace_id
    -        return ()
    -
    -
    -def test_computer_static_manifest_owns_workload_mcp_and_data() -> None:
    -    manifest = load_static_plugin_manifest(PLUGIN)
    -
    -    assert manifest.name == "computer"
    -    assert manifest.exclude_data_paths == (
    -        "state/config/pulse",
    -        "state/config/xfce4/desktop/icons.screen.latest.rc",
    -        "state/home/.opencli/node_modules",
    -        "state/profile/SingletonCookie",
    -        "state/profile/SingletonLock",
    -        "state/profile/SingletonSocket",
    -    )
    -    assert manifest.workloads[0].ports == (
    -        ("gateway", 8080),
    -        ("display", 6080),
    -        ("opencli", 19826),
    -    )
    -    assert manifest.workloads[0].loopback_ports == (("opencli", 19825),)
    -    assert manifest.workloads[0].data == (("state", "/data", True),)
    -    assert manifest.workloads[0].user_namespaces is True
    -    assert manifest.mcp_servers[0].workload_env == (
    -        ("COMPUTER_URL", "computer", "gateway"),
    -    )
    -    assert manifest.mcp_servers[0].required_tools == (
    -        "browser_observe",
    -        "browser_action",
    -        "computer_observe",
    -        "computer_action",
    -    )
    -
    -
    -def test_opencli_stays_a_skill_for_the_ordinary_shell() -> None:
    -    skill = (PLUGIN / "skills" / "opencli" / "SKILL.md").read_text(encoding="utf-8")
    -
    -    assert "name: opencli" in skill
    -    assert 'shell({"command":"opencli ' in skill
    -    assert "OPENCLI_DAEMON_PORT" not in skill
    -    assert 'browser({"args"' not in skill
    -    assert not (PLUGIN / "skills" / "computer" / "SKILL.md").exists()
    -
    -
    -def test_browser_ref_click_scrolls_before_reading_click_coordinates() -> None:
    -    gateway = (ROOT / "docker" / "computer" / "gateway.mjs").read_text(encoding="utf-8")
    -    click = gateway[gateway.index("async function clickNode") :]
    -
    -    assert click.index("DOM.scrollIntoViewIfNeeded") < click.index("DOM.getBoxModel")
    -
    -
    -def test_browser_ref_focus_keeps_the_backend_node_across_cdp_sessions() -> None:
    -    gateway = (ROOT / "docker" / "computer" / "gateway.mjs").read_text(encoding="utf-8")
    -    focus = gateway[
    -        gateway.index("async function focusNode") : gateway.index(
    -            "async function clickNode"
    -        )
    -    ]
    -
    -    assert '"DOM.focus", { backendNodeId }' in focus
    -    assert "DOM.resolveNode" not in focus
    -    assert "Runtime.callFunctionOn" not in focus
    -
    -
    -def test_browser_fill_uses_chromiums_select_all_edit_command() -> None:
    -    gateway = (ROOT / "docker" / "computer" / "gateway.mjs").read_text(encoding="utf-8")
    -    fill = gateway[
    -        gateway.index('if (action === "fill")') : gateway.index(
    -            'await cdp(selected.target, "Input.insertText"'
    -        )
    -    ]
    -
    -    assert 'commands: ["SelectAll"]' in fill
    -    assert "windowsVirtualKeyCode: 65" in fill
    -    assert "windowsVirtualKeyCode: 8" in fill
    -
    -
    -def test_computer_screenshot_files_are_bounded(
    -    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    -) -> None:
    -    data_dir = tmp_path / "computer-data"
    -    monkeypatch.setenv("AKA_PLUGIN_DATA_DIR", str(data_dir))
    -
    -    last = ""
    -    for _index in range(40):
    -        last = save_screenshot(_PNG, "image/png")
    -
    -    screenshots = tuple((data_dir / "screenshots").glob("computer-*.png"))
    -    assert len(screenshots) == 32
    -    assert Path(last).is_file()
    -
    -
    -def test_visual_drag_sends_intermediate_pointer_positions() -> None:
    -    gateway = (ROOT / "docker" / "computer" / "gateway.mjs").read_text(encoding="utf-8")
    -
    -    assert "const dragSteps = 8;" in gateway
    -    assert "for (let step = 1; step <= dragSteps; step += 1)" in gateway
    -    assert 'step === dragSteps ? "0.15" : "0.04"' in gateway
    -
    -
    -def test_dashboard_context_exposes_only_declared_workload_port(tmp_path: Path) -> None:
    -    context = DashboardContext(
    -        plugin_id="computer",
    -        plugin_dir=PLUGIN,
    -        data_root=tmp_path,
    -        validation=False,
    -        _workload_urls=MappingProxyType(
    -            {
    -                ("computer", "gateway"): "http://computer.internal:8080",
    -                ("computer", "display"): "http://computer.internal:6080",
    -            }
    -        ),
    -    )
    -
    -    assert (
    -        context.workload_url("computer", "gateway") == "http://computer.internal:8080"
    -    )
    -    with pytest.raises(CompositionError, match="未声明 Workload port"):
    -        context.workload_url("computer", "desktop")
    -
    -
    -def test_computer_dashboard_proxies_activity_and_live_display(tmp_path: Path) -> None:
    -    server = ThreadingHTTPServer(("127.0.0.1", 0), _Gateway)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    listener = socket.socket()
    -    listener.bind(("127.0.0.1", 0))
    -    listener.listen()
    -    display_closed = threading.Event()
    -
    -    def echo_display(connection) -> None:
    -        try:
    -            for message in connection:
    -                connection.send(message)
    -        finally:
    -            display_closed.set()
    -
    -    display_server = serve(
    -        echo_display,
    -        sock=listener,
    -        subprotocols=[Subprotocol("binary")],
    -        compression=None,
    -    )
    -    display_thread = threading.Thread(
    -        target=display_server.serve_forever,
    -        daemon=True,
    -    )
    -    display_thread.start()
    -    display_port = listener.getsockname()[1]
    -    context = DashboardContext(
    -        plugin_id="computer",
    -        plugin_dir=PLUGIN,
    -        data_root=tmp_path,
    -        validation=False,
    -        _workload_urls=MappingProxyType(
    -            {
    -                ("computer", "gateway"): (f"http://127.0.0.1:{server.server_port}"),
    -                ("computer", "display"): f"http://127.0.0.1:{display_port}",
    -            }
    -        ),
    -    )
    -    app = FastAPI()
    -    gateway_client = register_computer_dashboard(app, context)
    -    try:
    -        with TestClient(app) as client:
    -            assert (
    -                client.get("/api/dashboard/computer/activity").json()["noticeId"] == 1
    -            )
    -            with client.websocket_connect(
    -                "/api/dashboard/computer/display",
    -                subprotocols=["binary"],
    -            ) as display:
    -                assert display.accepted_subprotocol == "binary"
    -                display.send_bytes(b"RFB 003.008\n")
    -                assert display.receive_bytes() == b"RFB 003.008\n"
    -            assert display_closed.wait(timeout=5)
    -    finally:
    -        gateway_client.close()
    -        display_server.shutdown()
    -        display_thread.join(timeout=5)
    -        server.shutdown()
    -        server.server_close()
    -
    -
    -def test_computer_mcp_calls_exact_workload_gateway(tmp_path: Path) -> None:
    -    server = ThreadingHTTPServer(("127.0.0.1", 0), _Gateway)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    env = dict(os.environ)
    -    env["COMPUTER_URL"] = f"http://127.0.0.1:{server.server_port}"
    -    env["AKA_PLUGIN_DATA_DIR"] = str(tmp_path / "computer-data")
    -    process = subprocess.Popen(
    -        [str(PLUGIN / "mcp_server.py")],
    -        stdin=subprocess.PIPE,
    -        stdout=subprocess.PIPE,
    -        stderr=subprocess.PIPE,
    -        text=True,
    -        env=env,
    -    )
    -    assert process.stdin is not None and process.stdout is not None
    -    try:
    -        messages = (
    -            {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}},
    -            {"jsonrpc": "2.0", "method": "notifications/initialized"},
    -            {"jsonrpc": "2.0", "method": "tools/list", "params": {}},
    -            {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
    -            {
    -                "jsonrpc": "2.0",
    -                "id": 3,
    -                "method": "tools/call",
    -                "params": {
    -                    "name": "browser_observe",
    -                    "arguments": {"observe": "get_title"},
    -                },
    -            },
    -            {
    -                "jsonrpc": "2.0",
    -                "id": 4,
    -                "method": "tools/call",
    -                "params": {"name": "missing", "arguments": {}},
    -            },
    -            {
    -                "jsonrpc": "2.0",
    -                "id": 5,
    -                "method": "tools/call",
    -                "params": {
    -                    "name": "browser_observe",
    -                    "arguments": {"observe": "screenshot"},
    -                },
    -            },
    -            {
    -                "jsonrpc": "2.0",
    -                "id": 6,
    -                "method": "tools/call",
    -                "params": {
    -                    "name": "computer_observe",
    -                    "arguments": {"observe": "screenshot"},
    -                },
    -            },
    -        )
    -        for message in messages:
    -            process.stdin.write(json.dumps(message) + "\n")
    -        process.stdin.flush()
    -
    -        initialized = json.loads(process.stdout.readline())
    -        tools = json.loads(process.stdout.readline())
    -        call = json.loads(process.stdout.readline())
    -        failed_call = json.loads(process.stdout.readline())
    -        browser_screenshot = json.loads(process.stdout.readline())
    -        desktop_screenshot = json.loads(process.stdout.readline())
    -        assert initialized["result"]["protocolVersion"] == "2025-11-25"
    -        assert [item["name"] for item in tools["result"]["tools"]] == [
    -            "browser_observe",
    -            "browser_action",
    -            "computer_observe",
    -            "computer_action",
    -        ]
    -        browser_action = tools["result"]["tools"][1]
    -        browser_schema = browser_action["inputSchema"]["properties"]
    -        assert "navigate" in browser_schema["action"]["enum"]
    -        assert browser_schema["ref"]["pattern"].startswith("^e")
    -        assert browser_schema["snapshot_id"]["maxLength"] == 64
    -        action_tool = tools["result"]["tools"][3]
    -        action_schema = action_tool["inputSchema"]["properties"]
    -        assert "drag" in action_schema["action"]["enum"]
    -        assert action_schema["to_x"]["maximum"] == 1279
    -        assert action_schema["to_y"]["maximum"] == 799
    -        assert "Example Domain" in call["result"]["content"][0]["text"]
    -        assert failed_call["id"] == 4
    -        assert "unknown tool" in failed_call["error"]["message"]
    -        screenshot_dir = tmp_path / "computer-data" / "screenshots"
    -        screenshot_paths = []
    -        for response in (browser_screenshot, desktop_screenshot):
    -            assert response["result"]["content"][0]["type"] == "text"
    -            reference = json.loads(response["result"]["content"][0]["text"])
    -            assert reference["kind"] == "screenshot_file"
    -            assert reference["mime_type"] == "image/png"
    -            screenshot_path = Path(reference["path"])
    -            assert screenshot_path.parent == screenshot_dir
    -            assert screenshot_path.read_bytes() == _PNG
    -            assert "read_image_vision" in reference["next"]
    -            screenshot_paths.append(screenshot_path)
    -        assert screenshot_paths[0] != screenshot_paths[1]
    -    finally:
    -        process.terminate()
    -        process.wait(timeout=5)
    -        process.stdin.close()
    -        process.stdout.close()
    -        assert process.stderr is not None
    -        process.stderr.close()
    -        server.shutdown()
    -        server.server_close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_builtin_computer_loads_through_public_plugin_contract(
    -    tmp_path: Path,
    -) -> None:
    -    server = ThreadingHTTPServer(("127.0.0.1", 0), _Gateway)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    controller = _Controller(f"http://127.0.0.1:{server.server_port}")
    -    manager = PluginManager(
    -        plugin_dirs=[ROOT / "plugins" / "conversation_ui", PLUGIN],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "plugin-home" / "cache",
    -        workload_controller=controller,
    -    )
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("computer")
    -        assert generation is not None
    -        assert manager.workload_urls(generation.generation_id) == {
    -            ("computer", "gateway"): controller.endpoint,
    -            ("computer", "display"): controller.endpoint,
    -            ("computer", "opencli"): controller.endpoint,
    -        }
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.tool_registry is not None
    -        assert snapshot.tool_registry.get_tool_names_by_source("mcp", "computer") == {
    -            "mcp_computer__browser_observe",
    -            "mcp_computer__browser_action",
    -            "mcp_computer__computer_observe",
    -            "mcp_computer__computer_action",
    -        }
    -    finally:
    -        await manager.terminate_all()
    -        server.shutdown()
    -        server.server_close()
    -    assert len(controller.starts) == len(controller.stops) == 1
    diff --git a/tests/test_content_store.py b/tests/test_content_store.py
    deleted file mode 100644
    index 30660a10c..000000000
    --- a/tests/test_content_store.py
    +++ /dev/null
    @@ -1,1054 +0,0 @@
    -from __future__ import annotations
    -
    -import hashlib
    -import inspect
    -import sqlite3
    -from concurrent.futures import ThreadPoolExecutor
    -from datetime import UTC, datetime, timedelta
    -
    -import pytest
    -
    -from plugins.eventmail import plugin as content_plugin
    -from plugins.eventmail.store import EventMailIdentityConflict, EventMailStore
    -
    -
    -def _item(
    -    item_id: str,
    -    *,
    -    revision: str = "1",
    -    value: str | None = None,
    -    not_before: datetime | None = None,
    -    requires_ack: bool = True,
    -) -> dict[str, object]:
    -    return {
    -        "item_id": item_id,
    -        "revision": revision,
    -        "payload": {"value": value or item_id},
    -        "not_before": not_before,
    -        "requires_ack": requires_ack,
    -    }
    -
    -
    -def _select(store: EventMailStore, now: datetime, item_id: str = "one") -> str:
    -    snapshot = store.snapshot(now)
    -    candidate = next(
    -        item for item in snapshot["items"] if item["ref"]["item_id"] == item_id
    -    )
    -    result = store.select(
    -        candidate["ref"],
    -        snapshot["snapshot_seq"],
    -        {"session_id": "wake:fixture", "turn_id": f"turn:{item_id}"},
    -        now,
    -    )
    -    assert result["selected"] is True
    -    token = result["selection_token"]
    -    assert isinstance(token, str)
    -    return token
    -
    -
    -def test_submit_reuses_batch_receipt_and_revision_without_duplicate_item(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    items = [_item("one", not_before=now)]
    -
    -    first = store.submit("fitbit", "poll:1", items)
    -    repeated = store.submit("fitbit", "poll:1", items)
    -    another_batch = store.submit("fitbit", "poll:2", items)
    -
    -    assert first == repeated
    -    assert first["inserted"] == [
    -        {"source_id": "fitbit", "item_id": "one", "revision": "1"}
    -    ]
    -    assert another_batch["inserted"] == []
    -    assert another_batch["duplicates"] == first["inserted"]
    -    assert another_batch["high_watermark"] == 1
    -    assert store.state_counts() == {"pending": 1}
    -
    -
    -def test_missing_not_before_stays_idempotent_across_later_poll(tmp_path) -> None:
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    item = _item("one")
    -
    -    _ = store.submit("feed", "poll:1", [item])
    -    repeated = store.submit("feed", "poll:1", [item])
    -    later_poll = store.submit("feed", "poll:2", [item])
    -
    -    assert repeated["receipt_id"] == "content-submit:feed:poll:1"
    -    assert later_poll["duplicates"] == [
    -        {"source_id": "feed", "item_id": "one", "revision": "1"}
    -    ]
    -
    -
    -def test_expire_keeps_revision_and_removes_it_from_wake_snapshot(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    snapshot = store.snapshot(now)
    -    ref = snapshot["items"][0]["ref"]
    -
    -    result = store.expire((ref,), now)
    -
    -    assert result["expired"] == (ref,)
    -    assert store.snapshot(now)["items"] == ()
    -    revision = store.read_revision("feed", "one", "1")
    -    assert revision is not None
    -    assert revision["status"] == "expired"
    -    store.rebuild_mail_projections()
    -    assert store.read_revision("feed", "one", "1")["status"] == "expired"  # type: ignore[index]
    -
    -
    -def test_stable_batch_and_revision_identity_reject_different_content(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", value="a", not_before=now)])
    -
    -    with pytest.raises(EventMailIdentityConflict, match="batch identity"):
    -        store.submit("feed", "poll:1", [_item("one", value="b", not_before=now)])
    -    with pytest.raises(EventMailIdentityConflict, match="revision identity"):
    -        store.submit("feed", "poll:2", [_item("one", value="b", not_before=now)])
    -    with pytest.raises(EventMailIdentityConflict, match="revision identity"):
    -        store.submit(
    -            "feed",
    -            "poll:3",
    -            [_item("one", value="a", not_before=now + timedelta(seconds=1))],
    -        )
    -
    -
    -def test_frozen_high_watermark_selection_keeps_new_item_for_next_wake(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    frozen = store.snapshot(now)
    -
    -    _ = store.submit("feed", "poll:2", [_item("two", not_before=now)])
    -    selected = store.select(
    -        frozen["items"][0]["ref"],
    -        frozen["snapshot_seq"],
    -        {"session_id": "wake:fixture", "turn_id": "turn:one"},
    -        now,
    -    )
    -
    -    assert selected["selected"] is True
    -    assert selected["wake_needed"] is True
    -    assert [item["ref"]["item_id"] for item in store.snapshot(now)["items"]] == ["two"]
    -    assert store.state_counts() == {"pending": 1, "selected": 1}
    -
    -
    -def test_cas_selection_allows_only_one_turn_for_one_revision(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    snapshot = store.snapshot(now)
    -    ref = snapshot["items"][0]["ref"]
    -
    -    with ThreadPoolExecutor(max_workers=2) as executor:
    -        results = tuple(
    -            executor.map(
    -                lambda turn: store.select(
    -                    ref,
    -                    snapshot["snapshot_seq"],
    -                    {"session_id": "wake:fixture", "turn_id": turn},
    -                    now,
    -                ),
    -                ("turn:a", "turn:b"),
    -            )
    -        )
    -
    -    assert sum(result["selected"] is True for result in results) == 1
    -    assert store.state_counts() == {"selected": 1}
    -
    -
    -def test_selection_recovers_after_wake_loses_token(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    path = tmp_path / "content.sqlite3"
    -    store = EventMailStore(path)
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    snapshot = store.snapshot(now)
    -    accepted = {"session_id": "wake:recovery", "turn_id": "turn:accepted"}
    -    selected = store.select(
    -        snapshot["items"][0]["ref"], snapshot["snapshot_seq"], accepted, now
    -    )
    -
    -    restarted = EventMailStore(path)
    -    restarted.initialize()
    -    recovered = restarted.selection(accepted)
    -
    -    assert recovered is not None
    -    assert recovered["selection_token"] == selected["selection_token"]
    -    assert recovered["ref"]["item_id"] == "one"
    -    assert recovered["payload"] == {"value": "one"}
    -    assert recovered["status"] == "selected"
    -    assert recovered["accepted_turn"] == accepted
    -    assert "settlement_ref" not in recovered
    -
    -
    -def test_selected_recovers_same_tokens_in_snapshot_order_with_limit(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    path = tmp_path / "content.sqlite3"
    -    store = EventMailStore(path)
    -    _ = store.submit(
    -        "feed",
    -        "poll:1",
    -        (
    -            _item("one", not_before=now),
    -            _item("two", not_before=now),
    -            _item("three", not_before=now),
    -        ),
    -    )
    -    tokens = tuple(_select(store, now, item_id) for item_id in ("one", "two", "three"))
    -
    -    restarted = EventMailStore(path)
    -    recovered = restarted.selected(limit=2)
    -
    -    assert tuple(row["selection_token"] for row in recovered) == tokens[:2]
    -    assert tuple(row["ref"]["item_id"] for row in recovered) == ("one", "two")
    -    assert tuple(row["accepted_turn"] for row in recovered) == (
    -        {"session_id": "wake:fixture", "turn_id": "turn:one"},
    -        {"session_id": "wake:fixture", "turn_id": "turn:two"},
    -    )
    -    assert restarted.selected(limit=1) == recovered[:1]
    -
    -
    -def test_selected_excludes_ready_for_delivery_and_rejects_invalid_limit(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:1",
    -        (_item("one", not_before=now), _item("two", not_before=now)),
    -    )
    -    first = _select(store, now, "one")
    -    second = _select(store, now, "two")
    -    _ = store.transition(first, "ready_for_delivery")
    -
    -    assert tuple(row["selection_token"] for row in store.selected()) == (second,)
    -    for limit in (0, -1, True, 1.5):
    -        with pytest.raises(ValueError, match="limit 必须是正整数"):
    -            store.selected(limit)  # pyright: ignore[reportArgumentType]
    -
    -
    -def test_one_accepted_turn_cannot_select_two_items_concurrently(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:1",
    -        [_item("one", not_before=now), _item("two", not_before=now)],
    -    )
    -    snapshot = store.snapshot(now)
    -    accepted = {"session_id": "wake:one", "turn_id": "turn:shared"}
    -
    -    with ThreadPoolExecutor(max_workers=2) as executor:
    -        results = tuple(
    -            executor.map(
    -                lambda item: store.select(
    -                    item["ref"], snapshot["snapshot_seq"], accepted, now
    -                ),
    -                snapshot["items"],
    -            )
    -        )
    -
    -    assert sum(result["selected"] is True for result in results) == 1
    -    rejected = next(result for result in results if result["selected"] is False)
    -    assert rejected.get("reason") == "turn_already_selected"
    -    assert store.selection(accepted) is not None
    -    assert store.state_counts() == {"pending": 1, "selected": 1}
    -
    -
    -def test_selection_is_missing_or_isolated_by_full_turn_receipt(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:1",
    -        [_item("one", not_before=now), _item("two", not_before=now)],
    -    )
    -    snapshot = store.snapshot(now)
    -    first = {"session_id": "wake:a", "turn_id": "turn:same"}
    -    second = {"session_id": "wake:b", "turn_id": "turn:same"}
    -
    -    assert store.selection(first) is None
    -    assert (
    -        store.select(snapshot["items"][0]["ref"], snapshot["snapshot_seq"], first, now)[
    -            "selected"
    -        ]
    -        is True
    -    )
    -    assert (
    -        store.select(
    -            snapshot["items"][1]["ref"], snapshot["snapshot_seq"], second, now
    -        )["selected"]
    -        is True
    -    )
    -
    -    assert store.selection(first)["ref"]["item_id"] == "one"
    -    assert store.selection(second)["ref"]["item_id"] == "two"
    -    assert (
    -        store.selection({"session_id": "wake:missing", "turn_id": "turn:same"}) is None
    -    )
    -
    -
    -def test_deferred_selection_keeps_turn_owner_and_recovery_token(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:1",
    -        [_item("one", not_before=now), _item("two", not_before=now)],
    -    )
    -    snapshot = store.snapshot(now)
    -    accepted = {"session_id": "wake:defer", "turn_id": "turn:complete"}
    -    selected = store.select(
    -        snapshot["items"][0]["ref"], snapshot["snapshot_seq"], accepted, now
    -    )
    -    token = selected["selection_token"]
    -    assert isinstance(token, str)
    -    _ = store.transition(token, "defer", not_before=now)
    -    fresh = store.snapshot(now)
    -    other = next(item for item in fresh["items"] if item["ref"]["item_id"] == "two")
    -
    -    repeated = store.select(other["ref"], fresh["snapshot_seq"], accepted, now)
    -    recovered = store.selection(accepted)
    -
    -    assert repeated["selected"] is False
    -    assert repeated.get("reason") == "turn_already_selected"
    -    assert recovered is not None
    -    assert recovered["selection_token"] == token
    -    assert recovered["status"] == "deferred"
    -
    -
    -def test_item_state_version_rejects_stale_snapshot_after_defer(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    stale = store.snapshot(now)
    -    token = _select(store, now)
    -    _ = store.transition(token, "defer", not_before=now)
    -
    -    rejected = store.select(
    -        stale["items"][0]["ref"],
    -        stale["snapshot_seq"],
    -        {"session_id": "wake:fixture", "turn_id": "turn:stale"},
    -        now,
    -    )
    -    fresh = store.snapshot(now)
    -    accepted = store.select(
    -        fresh["items"][0]["ref"],
    -        fresh["snapshot_seq"],
    -        {"session_id": "wake:fixture", "turn_id": "turn:fresh"},
    -        now,
    -    )
    -
    -    assert rejected["selected"] is False
    -    assert accepted["selected"] is True
    -    assert accepted["accepted_turn"] == {
    -        "session_id": "wake:fixture",
    -        "turn_id": "turn:fresh",
    -    }
    -    connection = sqlite3.connect(store.path)
    -    selected_owner = connection.execute("""
    -        SELECT selected_session_id, selected_turn_id FROM items
    -        WHERE source_id = 'feed' AND item_id = 'one' AND revision = '1'
    -        """).fetchone()
    -    connection.close()
    -    assert selected_owner == ("wake:fixture", "turn:fresh")
    -
    -
    -def test_decline_transitions_recompute_wake_without_timer_state(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    token = _select(store, now)
    -    later = now + timedelta(hours=1)
    -
    -    deferred = store.transition(token, "defer", not_before=later)
    -    before_due = store.snapshot(now)
    -    at_due = store.snapshot(later)
    -
    -    assert deferred.get("status") == "deferred"
    -    assert before_due["wake_needed"] is True
    -    assert before_due["items"][0]["due"] is False
    -    assert at_due["items"][0]["due"] is True
    -
    -
    -def test_source_replay_ignores_content_owned_defer_deadline(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    item = _item("one", not_before=now)
    -    _ = store.submit("feed", "poll:1", [item])
    -    token = _select(store, now)
    -    retry_at = now + timedelta(minutes=5)
    -
    -    assert store.transition(token, "defer", not_before=retry_at)["changed"] is True
    -    replay = store.submit("feed", "poll:2", [item])
    -
    -    assert replay["inserted"] == []
    -    assert len(replay["duplicates"]) == 1
    -    revision = store.read_revision("feed", "one", "1")
    -    assert revision is not None
    -    assert revision["not_before"] == retry_at.isoformat()
    -
    -
    -def test_source_bound_unsettled_and_ack_cannot_cross_source(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("fitbit", "poll:1", [_item("sleep", not_before=now)])
    -    token = _select(store, now, "sleep")
    -    assert store.transition(token, "ready_for_delivery")["changed"] is True
    -    assert (
    -        store.transition(token, "delivered", settlement_ref="delivery:settle:1").get(
    -            "status"
    -        )
    -        == "delivered"
    -    )
    -
    -    assert store.unsettled("feed") == ()
    -    assert store.ack("feed", "delivery:settle:1") == {
    -        "settled": False,
    -        "reason": "settlement_missing",
    -    }
    -    assert [row["settlement_ref"] for row in store.unsettled("fitbit")] == [
    -        "delivery:settle:1"
    -    ]
    -    assert store.ack("fitbit", "delivery:settle:1") == {
    -        "settled": True,
    -        "duplicate": False,
    -    }
    -    assert store.ack("fitbit", "delivery:settle:1") == {
    -        "settled": True,
    -        "duplicate": True,
    -    }
    -    assert store.state_counts() == {"settled": 1}
    -
    -
    -def test_context_without_provider_ack_settles_at_delivery(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "steam",
    -        "poll:1",
    -        [_item("context", not_before=now, requires_ack=False)],
    -    )
    -    token = _select(store, now, "context")
    -    _ = store.transition(token, "ready_for_delivery")
    -
    -    delivered = store.transition(
    -        token, "delivered", settlement_ref="delivery:context:1"
    -    )
    -
    -    assert delivered.get("status") == "settled"
    -    assert store.unsettled("steam") == ()
    -    assert store.state_counts() == {"settled": 1}
    -
    -
    -@pytest.mark.parametrize(
    -    ("requires_ack", "expected_status"),
    -    ((True, "delivered"), (False, "settled")),
    -)
    -def test_delivery_capability_is_body_free_and_replays_stable_receipt(
    -    tmp_path,
    -    requires_ack: bool,
    -    expected_status: str,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "fitbit",
    -        "poll:delivery",
    -        [_item("delivery", not_before=now, requires_ack=requires_ack)],
    -    )
    -    token = _select(store, now, "delivery")
    -    _ = store.transition(token, "ready_for_delivery")
    -    accepted = {"session_id": "wake:fixture", "turn_id": "turn:delivery"}
    -    delivery = content_plugin._DeliveryServices(store)
    -
    -    assert delivery.pending() == (
    -        {
    -            "selection_token": token,
    -            "accepted_turn": accepted,
    -            "message_metadata": {
    -                "tools_used": ["message_push"],
    -                "evidence_item_ids": ["fitbit:delivery:1"],
    -                "source_refs": [{"display_index": 1, "event_id": "fitbit:delivery:1"}],
    -                "state_summary_tag": "none",
    -            },
    -            "decision_format": "items_v1",
    -        },
    -    )
    -    first = delivery.settle(token, "wake:logical-delivery")
    -    recovered = delivery.lookup(accepted)
    -    duplicate = delivery.settle(token, "wake:logical-delivery")
    -
    -    assert first["status"] == expected_status
    -    assert first["receipt"] == duplicate["receipt"]
    -    assert recovered == {
    -        "selection_token": token,
    -        "accepted_turn": accepted,
    -        "status": expected_status,
    -        "settlement_ref": "wake:logical-delivery",
    -        "receipt": first["receipt"],
    -    }
    -    assert delivery.pending() == ()
    -    assert recovered is not None
    -    assert set(recovered) == {
    -        "selection_token",
    -        "accepted_turn",
    -        "status",
    -        "settlement_ref",
    -        "receipt",
    -    }
    -
    -    if requires_ack:
    -        assert store.ack("fitbit", "wake:logical-delivery")["settled"] is True
    -        after_ack = delivery.lookup(accepted)
    -        assert after_ack is not None
    -        assert after_ack["status"] == "settled"
    -        assert after_ack["receipt"] == first["receipt"]
    -
    -
    -def test_skip_release_keeps_candidate_pending_without_source_ack(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:skip",
    -        [_item("skip", not_before=now, requires_ack=True)],
    -    )
    -    token = _select(store, now, "skip")
    -
    -    result = store.transition(token, "release")
    -
    -    assert result["changed"] is True and result.get("status") == "pending"
    -    assert store.state_counts() == {"pending": 1}
    -    assert store.unsettled("feed") == ()
    -
    -
    -def test_batch_share_projects_one_message_and_consumes_only_cited_members(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:batch",
    -        [
    -            {
    -                **_item(f"item:{index}", not_before=now, requires_ack=True),
    -                "payload": {
    -                    "title": f"Title {index}",
    -                    "url": f"https://example.test/{index}",
    -                },
    -            }
    -            for index in range(6)
    -        ],
    -    )
    -    snapshot = store.snapshot(now)
    -    accepted = {"session_id": "mobile:one", "turn_id": "turn:batch"}
    -    selected = store.select_batch(
    -        tuple(item["ref"] for item in snapshot["items"]),
    -        snapshot["snapshot_seq"],
    -        accepted,
    -        now,
    -    )
    -    token = selected["selection_token"]
    -    assert isinstance(token, str)
    -    cited = (snapshot["items"][1]["ref"], snapshot["items"][4]["ref"])
    -
    -    ready = store.transition(token, "ready_for_delivery", selected_refs=cited)
    -    pending = store.pending_delivery()
    -    settled = store.settle_delivery(token, "wake:batch")
    -
    -    assert ready.get("status") == "ready_for_delivery"
    -    assert len(pending) == 1
    -    assert pending[0]["accepted_turn"] == accepted
    -    metadata = pending[0]["message_metadata"]
    -    assert isinstance(metadata, dict)
    -    assert metadata["evidence_item_ids"] == [
    -        "feed:item:1:1",
    -        "feed:item:4:1",
    -    ]
    -    assert settled["settled"] is True
    -    assert store.state_counts() == {"delivered": 2, "pending": 4}
    -    acknowledgements = store.unsettled("feed")
    -    assert len(acknowledgements) == 2
    -    item_ids: set[str] = set()
    -    for row in acknowledgements:
    -        ref = row["ref"]
    -        assert isinstance(ref, dict)
    -        item_id = ref["item_id"]
    -        assert isinstance(item_id, str)
    -        item_ids.add(item_id)
    -    assert item_ids == {"item:1", "item:4"}
    -
    -
    -def test_batch_skip_releases_entire_candidate_page_without_ack(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit(
    -        "feed",
    -        "poll:batch-skip",
    -        [_item(f"item:{index}", not_before=now) for index in range(20)],
    -    )
    -    snapshot = store.snapshot(now)
    -    selected = store.select_batch(
    -        tuple(item["ref"] for item in snapshot["items"]),
    -        snapshot["snapshot_seq"],
    -        {"session_id": "mobile:one", "turn_id": "turn:skip"},
    -        now,
    -    )
    -    token = selected["selection_token"]
    -    assert isinstance(token, str)
    -
    -    result = store.transition(token, "release")
    -
    -    assert result.get("status") == "pending"
    -    assert store.state_counts() == {"pending": 20}
    -    assert len(store.snapshot(now)["items"]) == 20
    -    assert store.unsettled("feed") == ()
    -
    -
    -def test_delivery_capability_rejects_conflicting_settlement_identity(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("fitbit", "poll:1", [_item("one", not_before=now)])
    -    token = _select(store, now)
    -    _ = store.transition(token, "ready_for_delivery")
    -    delivery = content_plugin._DeliveryServices(store)
    -    _ = delivery.settle(token, "wake:one")
    -
    -    with pytest.raises(RuntimeError, match="settlement identity conflict"):
    -        delivery.settle(token, "wake:two")
    -
    -
    -def test_wake_capability_cannot_commit_delivery_but_can_abandon_ready_item(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    token = _select(store, now)
    -    wake = content_plugin._WakeServices(store)
    -    assert wake.transition(token, "ready_for_delivery")["changed"] is True
    -
    -    with pytest.raises(ValueError, match="不拥有 transition: delivered"):
    -        wake.transition(token, "delivered")
    -    assert "settlement_ref" not in inspect.signature(wake.transition).parameters
    -    assert wake.transition(token, "abandoned")["status"] == "abandoned"
    -
    -
    -@pytest.mark.parametrize(
    -    "action",
    -    (
    -        "ready_for_delivery",
    -        "defer",
    -        "await_change",
    -        "invalidated",
    -        "abandoned",
    -        "expired",
    -    ),
    -)
    -def test_non_delivery_transition_cannot_write_settlement_ref(
    -    tmp_path, action: str
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "content.sqlite3")
    -    _ = store.submit("feed", "poll:1", [_item("one", not_before=now)])
    -    token = _select(store, now)
    -    not_before = now + timedelta(hours=1) if action == "defer" else None
    -
    -    with pytest.raises(ValueError, match="只有 delivered"):
    -        store.transition(
    -            token,
    -            action,
    -            not_before=not_before,
    -            settlement_ref="delivery:forbidden",
    -        )
    -
    -    connection = sqlite3.connect(store.path)
    -    persisted = connection.execute(
    -        "SELECT status, settlement_ref FROM items WHERE selection_token = ?",
    -        (token,),
    -    ).fetchone()
    -    connection.close()
    -    assert persisted == ("selected", None)
    -
    -
    -def test_source_id_has_one_bound_owner_per_root(tmp_path) -> None:
    -    services = content_plugin._SourceServices(
    -        EventMailStore(tmp_path / "content.sqlite3"),
    -        lambda: None,
    -    )
    -    first = services.bind("fitbit")
    -
    -    with pytest.raises(RuntimeError, match="已有 owner"):
    -        services.bind("fitbit")
    -
    -    assert first.unsettled() == ()
    -
    -
    -def test_source_bound_exact_reads_do_not_write_or_emit_change(tmp_path) -> None:
    -    path = tmp_path / "content.sqlite3"
    -    changed = 0
    -
    -    def record_changed() -> None:
    -        nonlocal changed
    -        changed += 1
    -
    -    bound = content_plugin._SourceServices(EventMailStore(path), record_changed).bind(
    -        "feed-subscriptions"
    -    )
    -    receipt = bound.submit("legacy:event-1", [_item("event-1", revision="rev-1")])
    -    assert changed == 1
    -    before = {
    -        entry.name: hashlib.sha256(entry.read_bytes()).hexdigest()
    -        for entry in tmp_path.iterdir()
    -        if entry.is_file()
    -    }
    -
    -    assert bound.read_submission("legacy:event-1") == receipt
    -    assert bound.read_revision("event-1", "rev-1")["ref"] == {
    -        "source_id": "feed-subscriptions",
    -        "item_id": "event-1",
    -        "revision": "rev-1",
    -    }
    -
    -    after = {
    -        entry.name: hashlib.sha256(entry.read_bytes()).hexdigest()
    -        for entry in tmp_path.iterdir()
    -        if entry.is_file()
    -    }
    -    assert changed == 1
    -    assert after == before
    -
    -
    -def test_exact_read_rejects_uncheckpointed_wal_instead_of_missing_row(tmp_path) -> None:
    -    path = tmp_path / "content.sqlite3"
    -    store = EventMailStore(path)
    -    _ = store.submit("feed-subscriptions", "one", [_item("one")])
    -    writer = sqlite3.connect(path)
    -    _ = writer.execute("PRAGMA journal_mode = WAL")
    -    _ = writer.execute(
    -        "UPDATE content_state SET state_version = state_version + 1 WHERE singleton=1"
    -    )
    -    writer.commit()
    -    assert path.with_name(path.name + "-wal").stat().st_size > 0
    -
    -    with pytest.raises(RuntimeError, match="checkpointed offline store"):
    -        store.read_submission("feed-subscriptions", "one")
    -
    -    writer.close()
    -    assert store.read_submission("feed-subscriptions", "one") is not None
    -
    -
    -def test_initialize_rejects_unknown_or_malformed_schema(tmp_path) -> None:
    -    unknown = tmp_path / "unknown.sqlite3"
    -    connection = sqlite3.connect(unknown)
    -    connection.execute("PRAGMA user_version = 99")
    -    connection.close()
    -    with pytest.raises(RuntimeError, match="schema version: 99"):
    -        EventMailStore(unknown).initialize()
    -
    -    malformed = tmp_path / "malformed.sqlite3"
    -    connection = sqlite3.connect(malformed)
    -    connection.execute("CREATE TABLE items(wrong TEXT)")
    -    connection.execute("PRAGMA user_version = 1")
    -    connection.close()
    -    with pytest.raises(RuntimeError, match="schema mismatch"):
    -        EventMailStore(malformed).initialize()
    -
    -
    -def test_v1_single_item_selection_migrates_to_exact_batch_ledger(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    path = tmp_path / "content-v1.sqlite3"
    -    store = EventMailStore(path)
    -    _ = store.submit("feed", "poll:v1", [_item("one", not_before=now)])
    -    token = _select(store, now)
    -    _ = store.transition(token, "ready_for_delivery")
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        DROP INDEX content_selection_members_order_idx;
    -        DROP INDEX content_selection_status_idx;
    -        DROP TABLE content_selection_members;
    -        DROP TABLE content_selections;
    -        DROP INDEX mail_transitions_mail_seq_idx;
    -        DROP INDEX mail_envelopes_kind_seq_idx;
    -        DROP INDEX alert_projection_due_idx;
    -        DROP INDEX context_projection_expiry_idx;
    -        DROP TABLE alert_projection;
    -        DROP TABLE context_projection;
    -        DROP TABLE mail_transitions;
    -        DROP TABLE mail_envelopes;
    -        PRAGMA user_version = 1;
    -        """)
    -    connection.close()
    -
    -    migrated = EventMailStore(path)
    -    migrated.initialize()
    -    recovered = migrated.selection(
    -        {"session_id": "wake:fixture", "turn_id": "turn:one"}
    -    )
    -
    -    assert recovered is not None
    -    assert recovered["status"] == "ready_for_delivery"
    -    assert len(recovered["items"]) == 1
    -    assert migrated.pending_delivery()[0]["selection_token"] == token
    -    with sqlite3.connect(path) as connection:
    -        assert connection.execute("PRAGMA user_version").fetchone()[0] == 3
    -    assert connection.execute("PRAGMA integrity_check").fetchone()[0] == "ok"
    -    connection.close()
    -
    -
    -def test_initialize_rejects_constraint_free_schema_with_same_columns(tmp_path) -> None:
    -    path = tmp_path / "lookalike.sqlite3"
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        CREATE TABLE content_state(
    -            singleton INTEGER,
    -            next_seq INTEGER NOT NULL,
    -            state_version INTEGER NOT NULL,
    -            wake_needed INTEGER NOT NULL,
    -            earliest_not_before TEXT
    -        );
    -        INSERT INTO content_state VALUES(1, 0, 0, 0, NULL);
    -        CREATE TABLE items(
    -            source_id TEXT NOT NULL,
    -            item_id TEXT NOT NULL,
    -            revision TEXT NOT NULL,
    -            payload_json TEXT NOT NULL,
    -            snapshot_seq INTEGER NOT NULL,
    -            status TEXT NOT NULL,
    -            not_before TEXT NOT NULL,
    -            requires_ack INTEGER NOT NULL,
    -            item_state_version INTEGER NOT NULL,
    -            selection_token TEXT,
    -            selected_session_id TEXT,
    -            selected_turn_id TEXT,
    -            settlement_ref TEXT,
    -            created_at TEXT NOT NULL,
    -            updated_at TEXT NOT NULL
    -        );
    -        CREATE TABLE submissions(
    -            source_id TEXT NOT NULL,
    -            batch_id TEXT NOT NULL,
    -            fingerprint TEXT NOT NULL,
    -            receipt_json TEXT NOT NULL,
    -            submitted_at TEXT NOT NULL
    -        );
    -        PRAGMA user_version = 1;
    -        """)
    -    connection.close()
    -
    -    with pytest.raises(RuntimeError, match="schema mismatch"):
    -        EventMailStore(path).initialize()
    -
    -
    -def test_initialize_rejects_missing_index_and_singleton_row(tmp_path) -> None:
    -    missing_index = EventMailStore(tmp_path / "missing-index.sqlite3")
    -    missing_index.initialize()
    -    connection = sqlite3.connect(missing_index.path)
    -    connection.execute("DROP INDEX items_wake_idx")
    -    connection.commit()
    -    connection.close()
    -    with pytest.raises(RuntimeError, match="items indexes"):
    -        missing_index.initialize()
    -
    -    missing_state = EventMailStore(tmp_path / "missing-state.sqlite3")
    -    missing_state.initialize()
    -    connection = sqlite3.connect(missing_state.path)
    -    connection.execute("DELETE FROM content_state")
    -    connection.commit()
    -    connection.close()
    -    with pytest.raises(RuntimeError, match="content_state singleton row"):
    -        missing_state.initialize()
    -
    -
    -def test_initialize_rejects_physically_corrupt_sqlite(tmp_path) -> None:
    -    path = tmp_path / "corrupt.sqlite3"
    -    path.write_bytes(b"not a sqlite database")
    -
    -    with pytest.raises(sqlite3.DatabaseError, match="database|encrypted"):
    -        EventMailStore(path).initialize()
    -
    -
    -def test_alert_and_context_keep_separate_eventmail_lifecycles(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 9, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "eventmail.sqlite3")
    -
    -    store.report_alert(
    -        source_id="calendar",
    -        event_id="meeting",
    -        payload={"title": "Meeting soon"},
    -        observed_at=now,
    -        expires_at=now + timedelta(minutes=5),
    -    )
    -    store.report_context(
    -        source_id="steam",
    -        event_id="current",
    -        payload={"presence": "active"},
    -        observed_at=now,
    -        expires_at=now + timedelta(minutes=10),
    -    )
    -    selected = store.select_alert(
    -        {"session_id": "mobile:one", "turn_id": "turn:alert"}, now
    -    )
    -
    -    assert selected is not None
    -    store.close_alert("calendar", "meeting", "delivered")
    -    assert store.alert_status("calendar", "meeting") == "delivered"
    -    assert store.active_context(now)[0]["payload"] == {"presence": "active"}
    -    assert store.mail_watermark() == 2
    -
    -
    -def test_older_alert_and_context_replay_cannot_replace_newer_projection(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 9, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "eventmail.sqlite3")
    -    store.report_alert(
    -        source_id="calendar",
    -        event_id="meeting",
    -        payload={"revision": "new"},
    -        observed_at=now,
    -    )
    -    store.report_context(
    -        source_id="steam",
    -        event_id="current",
    -        payload={"revision": "new"},
    -        observed_at=now,
    -        expires_at=None,
    -    )
    -
    -    alert = store.report_alert(
    -        source_id="calendar",
    -        event_id="meeting",
    -        payload={"revision": "old"},
    -        observed_at=now - timedelta(minutes=1),
    -    )
    -    context = store.report_context(
    -        source_id="steam",
    -        event_id="current",
    -        payload={"revision": "old"},
    -        observed_at=now - timedelta(minutes=1),
    -        expires_at=None,
    -    )
    -
    -    assert alert["accepted"] is True and alert["projected"] is False
    -    assert context["accepted"] is True and context["projected"] is False
    -    selected = store.select_alert(
    -        {"session_id": "mobile:one", "turn_id": "turn:alert"}, now
    -    )
    -    assert selected is not None and selected["payload"] == {"revision": "new"}
    -    assert store.active_context(now)[0]["payload"] == {"revision": "new"}
    -
    -
    -def test_alert_and_context_projections_rebuild_from_immutable_history(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 9, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "eventmail.sqlite3")
    -    store.report_alert(
    -        source_id="calendar",
    -        event_id="meeting",
    -        payload={"title": "Meeting"},
    -        observed_at=now,
    -    )
    -    selected = store.select_alert(
    -        {"session_id": "mobile:one", "turn_id": "turn:alert"}, now
    -    )
    -    assert selected is not None
    -    deferred_until = now + timedelta(minutes=2)
    -    store.defer_alert("calendar", "meeting", deferred_until)
    -    store.report_context(
    -        source_id="steam",
    -        event_id="current",
    -        payload={"presence": "active"},
    -        observed_at=now,
    -        expires_at=None,
    -    )
    -    connection = sqlite3.connect(store.path)
    -    connection.execute("DELETE FROM alert_projection")
    -    connection.execute("DELETE FROM context_projection")
    -    connection.commit()
    -    connection.close()
    -
    -    store.rebuild_mail_projections()
    -
    -    assert store.alert_status("calendar", "meeting") == "pending"
    -    assert store.alert_deadline(now) == deferred_until
    -    assert store.active_context(now)[0]["payload"] == {"presence": "active"}
    -
    -
    -def test_content_projection_rebuilds_exactly_from_immutable_history(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 9, tzinfo=UTC)
    -    store = EventMailStore(tmp_path / "eventmail.sqlite3")
    -    store.submit(
    -        "feed",
    -        "batch:one",
    -        (
    -            _item("one", not_before=now, requires_ack=True),
    -            _item("two", not_before=now, requires_ack=False),
    -        ),
    -    )
    -    snapshot = store.snapshot(now)
    -    selected = store.select_batch(
    -        tuple(item["ref"] for item in snapshot["items"]),
    -        snapshot["snapshot_seq"],
    -        {"session_id": "wake:fixture", "turn_id": "turn:projection"},
    -        now,
    -    )
    -    token = selected["selection_token"]
    -    assert isinstance(token, str)
    -    assert (
    -        store.transition(
    -            token,
    -            "ready_for_delivery",
    -            selected_refs=tuple(item["ref"] for item in snapshot["items"]),
    -        )["changed"]
    -        is True
    -    )
    -    settled = store.settle_delivery(token, "delivery:projection")
    -    assert settled["settled"] is True
    -    delivery = store.delivery(
    -        {"session_id": "wake:fixture", "turn_id": "turn:projection"}
    -    )
    -    assert delivery is not None
    -    first = store.read_revision("feed", "one", "1")
    -    assert first is not None
    -    connection = sqlite3.connect(store.path)
    -    ack_ref = connection.execute(
    -        "SELECT settlement_ref FROM items WHERE source_id='feed' AND item_id='one'"
    -    ).fetchone()[0]
    -    connection.close()
    -    assert store.ack("feed", str(ack_ref))["settled"] is True
    -
    -    tables = (
    -        "content_state",
    -        "items",
    -        "content_selections",
    -        "content_selection_members",
    -    )
    -
    -    def rows(table: str) -> list[tuple[object, ...]]:
    -        database = sqlite3.connect(store.path)
    -        try:
    -            return database.execute(f"SELECT * FROM {table} ORDER BY rowid").fetchall()
    -        finally:
    -            database.close()
    -
    -    expected = {table: rows(table) for table in tables}
    -    database = sqlite3.connect(store.path)
    -    try:
    -        database.execute("DELETE FROM content_selection_members")
    -        database.execute("DELETE FROM content_selections")
    -        database.execute("DELETE FROM items")
    -        database.execute("DELETE FROM content_state")
    -        database.commit()
    -    finally:
    -        database.close()
    -
    -    store.rebuild_mail_projections()
    -
    -    assert {table: rows(table) for table in tables} == expected
    -    assert expected["items"][0][7] == 1
    diff --git a/tests/test_content_v3_composition.py b/tests/test_content_v3_composition.py
    deleted file mode 100644
    index 6dffe7462..000000000
    --- a/tests/test_content_v3_composition.py
    +++ /dev/null
    @@ -1,605 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import hashlib
    -import shutil
    -import sqlite3
    -import threading
    -import time
    -from collections.abc import Mapping
    -from datetime import UTC, datetime, timedelta
    -from pathlib import Path
    -from typing import Any, cast
    -
    -import pytest
    -
    -import agent.plugins.manager as plugin_manager_module
    -from agent.control.timer import TimerReceipt, TimerStatus
    -from agent.plugin_composition import ServiceKey
    -from agent.plugin_composition.timers import PluginTimers
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -from plugins.eventmail import plugin as content_plugin
    -from plugins.eventmail.plugin import ContentSourceServices, ContentWakeServices
    -from plugins.eventmail.store import ContentSnapshot, EventMailStore
    -from tests.fixtures.content_clock_source.plugin import (
    -    BoundContentSource,
    -    FixtureSourceStore,
    -    SourceRuntime,
    -)
    -from tests.fixtures.content_hint_probe.plugin import CONTENT_HINT_PROBE
    -
    -EVENTMAIL_CONTENT_SOURCE = ServiceKey[ContentSourceServices](
    -    "eventmail.content_source.v1"
    -)
    -EVENTMAIL_WAKE = ServiceKey[ContentWakeServices]("eventmail.wake.v1")
    -
    -
    -class _TimerHandle:
    -    def __init__(self, timer_id: str, deadline: datetime, now: datetime) -> None:
    -        self._id = timer_id
    -        self.deadline = deadline
    -        self.now = now
    -        self.future: asyncio.Future[TimerReceipt] = (
    -            asyncio.get_running_loop().create_future()
    -        )
    -
    -    @property
    -    def id(self) -> str:
    -        return self._id
    -
    -    async def result(self) -> TimerReceipt:
    -        return await asyncio.shield(self.future)
    -
    -    async def cancel(self) -> TimerReceipt:
    -        if not self.future.done():
    -            self.future.set_result(self._receipt(TimerStatus.CANCELLED))
    -        return await self.future
    -
    -    async def cleanup(self) -> None:
    -        _ = await self.cancel()
    -
    -    def fire(self) -> None:
    -        self.future.set_result(self._receipt(TimerStatus.FIRED))
    -
    -    def _receipt(self, status: TimerStatus) -> TimerReceipt:
    -        return TimerReceipt(self.id, self.deadline, self.now, status)
    -
    -
    -class _Timer:
    -    def __init__(self, now: datetime) -> None:
    -        self.now = now
    -        self.handles: list[_TimerHandle] = []
    -
    -    def schedule(self, deadline: datetime) -> _TimerHandle:
    -        handle = _TimerHandle(f"timer:{len(self.handles)}", deadline, self.now)
    -        self.handles.append(handle)
    -        return handle
    -
    -
    -async def _eventually(predicate) -> None:
    -    for _ in range(200):
    -        if predicate():
    -            return
    -        await asyncio.sleep(0.01)
    -    raise AssertionError("condition did not settle")
    -
    -
    -def _copy_plugins(tmp_path: Path) -> tuple[Path, Path, Path]:
    -    root = Path(__file__).resolve().parents[1]
    -    content = tmp_path / "plugins" / "eventmail"
    -    source = tmp_path / "plugins" / "content_clock_source"
    -    probe = tmp_path / "plugins" / "content_hint_probe"
    -    shutil.copytree(root / "plugins" / "eventmail", content)
    -    shutil.copytree(
    -        root / "tests" / "fixtures" / "content_clock_source",
    -        source,
    -    )
    -    shutil.copytree(
    -        root / "tests" / "fixtures" / "content_hint_probe",
    -        probe,
    -    )
    -    return content, source, probe
    -
    -
    -def _sqlite_hashes(path: Path) -> dict[str, str]:
    -    return {
    -        candidate.name: hashlib.sha256(candidate.read_bytes()).hexdigest()
    -        for candidate in sorted(path.parent.glob(path.name + "*"))
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_real_v3_loader_timer_and_stores_submit_before_cursor(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    timer = _Timer(now)
    -    monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", lambda: timer)
    -    content_dir, source_dir, probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    source_store = FixtureSourceStore(
    -        workspace / "plugin-data" / "content_clock_source-builtin" / "source.sqlite3"
    -    )
    -    source_store.seed(({"kind": "sleep", "score": 92},), now)
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir, source_dir, probe_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -    try:
    -        await _eventually(lambda: len(timer.handles) == 1)
    -        timer.handles[0].fire()
    -        await _eventually(lambda: source_store.state(now)["cursor"] == 1)
    -
    -        content_store = EventMailStore(
    -            workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -        )
    -        assert content_store.state_counts() == {"pending": 1}
    -        assert source_store.state(now)["poll_count"] == 1
    -        assert len(timer.handles) == 2
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        wake = snapshot.composition_root.context.require(EVENTMAIL_WAKE)
    -        assert wake.snapshot(now)["wake_needed"] is True
    -    finally:
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_hint_listener_failure_repolls_before_cursor_without_duplicate_content(
    -    tmp_path: Path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    source_store = FixtureSourceStore(tmp_path / "source.sqlite3")
    -    source_store.seed(({"kind": "feed"},), now)
    -    content_store = EventMailStore(tmp_path / "content.sqlite3")
    -
    -    visible_snapshots: list[ContentSnapshot] = []
    -
    -    def changed() -> None:
    -        visible_snapshots.append(content_store.snapshot(now))
    -        if len(visible_snapshots) == 1:
    -            raise RuntimeError("hint listener failed")
    -
    -    bound = content_plugin._SourceServices(content_store, changed).bind("clock-feed")
    -    successful_receipts: list[Mapping[str, object]] = []
    -
    -    class RecordingBound:
    -        def submit(self, batch_id, items):
    -            receipt = bound.submit(batch_id, items)
    -            successful_receipts.append(receipt)
    -            return receipt
    -
    -    first_timer = _Timer(now)
    -
    -    first = SourceRuntime(
    -        source_store,
    -        PluginTimers(first_timer),
    -        cast(BoundContentSource, RecordingBound()),
    -        now=lambda: now,
    -    )
    -    await first.start()
    -    task = first._task
    -    assert task is not None
    -    first_timer.handles[0].fire()
    -    with pytest.raises(RuntimeError, match="hint listener failed"):
    -        await task
    -
    -    assert source_store.state(now)["cursor"] == 0
    -    assert content_store.state_counts() == {"pending": 1}
    -    cursor, items = source_store.poll()
    -    persisted_receipt = content_store.submit("clock-feed", "poll:0:1", items)
    -    assert cursor == 0
    -    assert len(visible_snapshots) == 1
    -    assert visible_snapshots[0]["items"][0]["ref"]["item_id"] == "event-1"
    -
    -    second_timer = _Timer(now)
    -    second = SourceRuntime(
    -        source_store,
    -        PluginTimers(second_timer),
    -        cast(BoundContentSource, RecordingBound()),
    -        now=lambda: now,
    -    )
    -    await second.start()
    -    second_timer.handles[0].fire()
    -    await _eventually(lambda: source_store.state(now)["cursor"] == 1)
    -
    -    assert content_store.state_counts() == {"pending": 1}
    -    assert source_store.state(now)["poll_count"] == 1
    -    assert len(visible_snapshots) == 2
    -    assert successful_receipts == [persisted_receipt]
    -    await second.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_content_submit_without_changed_listener_still_succeeds(
    -    tmp_path: Path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    content_dir, _source_dir, _probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    try:
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        source = snapshot.composition_root.context.require(
    -            EVENTMAIL_CONTENT_SOURCE
    -        ).bind("no-listener")
    -
    -        receipt = source.submit(
    -            "poll:1",
    -            (
    -                {
    -                    "item_id": "one",
    -                    "revision": "1",
    -                    "payload": {"kind": "no-listener"},
    -                    "not_before": now,
    -                },
    -            ),
    -        )
    -
    -        assert receipt["inserted"] == [
    -            {"source_id": "no-listener", "item_id": "one", "revision": "1"}
    -        ]
    -    finally:
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_root_has_no_timer_poll_or_formal_write(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    timers: list[_Timer] = []
    -
    -    def timer_factory() -> _Timer:
    -        timer = _Timer(now)
    -        timers.append(timer)
    -        return timer
    -
    -    monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", timer_factory)
    -    content_dir, source_dir, probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    source_store = FixtureSourceStore(
    -        workspace / "plugin-data" / "content_clock_source-builtin" / "source.sqlite3"
    -    )
    -    source_store.seed(({"kind": "calendar"},), now + timedelta(hours=1))
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir, source_dir, probe_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -    formal_reader: sqlite3.Connection | None = None
    -    try:
    -        await _eventually(lambda: sum(len(timer.handles) for timer in timers) == 1)
    -        before = source_store.state(now)
    -        content_path = (
    -            workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -        )
    -        assert content_path.is_file()
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        wake = snapshot.composition_root.context.require(EVENTMAIL_WAKE)
    -        hint_probe = snapshot.composition_root.context.require(CONTENT_HINT_PROBE)
    -        assert hint_probe.count == 0
    -        content = snapshot.composition_root.context.require(
    -            EVENTMAIL_CONTENT_SOURCE
    -        ).bind("candidate-probe")
    -        receipt = content.submit(
    -            "poll:1",
    -            (
    -                {
    -                    "item_id": "candidate-row",
    -                    "revision": "1",
    -                    "payload": {"kind": "candidate"},
    -                    "not_before": now,
    -                },
    -            ),
    -        )
    -        assert receipt["high_watermark"] == 1
    -        repeated = content.submit(
    -            "poll:1",
    -            (
    -                {
    -                    "item_id": "candidate-row",
    -                    "revision": "1",
    -                    "payload": {"kind": "candidate"},
    -                    "not_before": now,
    -                },
    -            ),
    -        )
    -        assert repeated == receipt
    -        assert hint_probe.count == 2
    -        visible_counts: list[int] = []
    -        for view in hint_probe.snapshots:
    -            items = view["items"]
    -            assert isinstance(items, tuple)
    -            visible_counts.append(len(items))
    -        assert visible_counts == [1, 1]
    -        frozen = cast(ContentSnapshot, wake.snapshot(now))
    -        accepted = {
    -            "session_id": "wake:candidate",
    -            "turn_id": "turn:accepted",
    -        }
    -        selected = wake.select(
    -            frozen["items"][0]["ref"], frozen["snapshot_seq"], accepted, now
    -        )
    -        assert selected["selected"] is True
    -        formal_reader = sqlite3.connect(content_path)
    -        assert formal_reader.execute("SELECT COUNT(*) FROM items").fetchone() == (1,)
    -        formal_hashes = _sqlite_hashes(content_path)
    -        formal_mtimes = {
    -            path.name: path.stat().st_mtime_ns
    -            for path in content_path.parent.glob(content_path.name + "*")
    -        }
    -
    -        with (source_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -            handle.write("\n# candidate fixture revision\n")
    -        candidate = await manager.prepare_candidate("content_clock_source")
    -
    -        assert candidate is not None and candidate.runtime_snapshot is not None
    -        candidate_content = candidate.runtime_snapshot.generations["eventmail"]
    -        candidate_root = candidate.runtime_snapshot.composition_root
    -        assert candidate_root is not None
    -        candidate_runtime = candidate_root.plugin_runtime("eventmail")
    -        candidate_path = candidate_runtime.data_dir / "eventmail.sqlite3"
    -        assert candidate_content.static_manifest is not None
    -        assert candidate_path != content_path
    -        candidate_wake = candidate_root.context.require(EVENTMAIL_WAKE)
    -        candidate_source = candidate_root.context.require(
    -            EVENTMAIL_CONTENT_SOURCE
    -        ).bind("candidate-write-probe")
    -        candidate_hint_probe = candidate_root.context.require(CONTENT_HINT_PROBE)
    -        assert candidate_hint_probe is not hint_probe
    -        assert candidate_hint_probe.count == 0
    -        recovered = candidate_wake.selection(accepted)
    -        assert recovered is not None
    -        assert recovered["selection_token"] == selected["selection_token"]
    -        assert "settlement_ref" not in recovered
    -        assert candidate_wake.selected() == (recovered,)
    -        assert candidate_wake.snapshot(now)["items"] == ()
    -        candidate_source.submit(
    -            "poll:1",
    -            (
    -                {
    -                    "item_id": "candidate-only",
    -                    "revision": "1",
    -                    "payload": {"kind": "candidate-write"},
    -                    "not_before": now,
    -                },
    -            ),
    -        )
    -        assert candidate_hint_probe.count == 1
    -        assert sum(len(timer.handles) for timer in timers) == 1
    -        assert source_store.state(now) == before
    -        assert _sqlite_hashes(content_path) == formal_hashes
    -        assert {
    -            path.name: path.stat().st_mtime_ns
    -            for path in content_path.parent.glob(content_path.name + "*")
    -        } == formal_mtimes
    -        await manager.discard_prepared("content_clock_source")
    -        assert content_path.is_file()
    -        assert EventMailStore(content_path).selection(accepted) == recovered
    -    finally:
    -        if formal_reader is not None:
    -            formal_reader.close()
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_promotion_drains_old_wait_and_only_new_root_recovers(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    timers: list[_Timer] = []
    -
    -    def timer_factory() -> _Timer:
    -        timer = _Timer(now)
    -        timers.append(timer)
    -        return timer
    -
    -    monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", timer_factory)
    -    content_dir, source_dir, probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    source_store = FixtureSourceStore(
    -        workspace / "plugin-data" / "content_clock_source-builtin" / "source.sqlite3"
    -    )
    -    source_store.seed(({"kind": "future"},), now + timedelta(hours=1))
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir, source_dir, probe_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -    try:
    -        await _eventually(lambda: sum(len(timer.handles) for timer in timers) == 1)
    -        old_handle = next(timer.handles[0] for timer in timers if timer.handles)
    -
    -        with (source_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -            handle.write("\n# promoted fixture revision\n")
    -        assert await manager.prepare_candidate("content_clock_source") is not None
    -        result = await manager.publish_prepared("content_clock_source")
    -
    -        assert result["publication_state"] == "committed"
    -        await _eventually(
    -            lambda: sum(
    -                not handle.future.done() for timer in timers for handle in timer.handles
    -            )
    -            == 1
    -        )
    -        assert old_handle.future.result().status is TimerStatus.CANCELLED
    -        assert source_store.state(now)["poll_count"] == 0
    -    finally:
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_shared_candidate_stays_readable_during_concurrent_submit(
    -    tmp_path: Path,
    -) -> None:
    -    now = datetime(2026, 8, 23, 5, tzinfo=UTC)
    -    content_dir, source_dir, probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir, source_dir, probe_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    started = threading.Event()
    -    stop = threading.Event()
    -    written = 0
    -    candidate = None
    -    writer: asyncio.Task[None] | None = None
    -
    -    def submit_until_stopped() -> None:
    -        nonlocal written
    -        while not stop.is_set() and written < 2_000:
    -            sequence = written + 1
    -            _ = source.submit(
    -                f"poll:{sequence}",
    -                (
    -                    {
    -                        "item_id": f"item-{sequence}",
    -                        "revision": "1",
    -                        "payload": {"sequence": sequence},
    -                        "not_before": now,
    -                    },
    -                ),
    -            )
    -            written = sequence
    -            started.set()
    -            time.sleep(0.0005)
    -
    -    try:
    -        await manager.load_all()
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        source = snapshot.composition_root.context.require(
    -            EVENTMAIL_CONTENT_SOURCE
    -        ).bind("clone-stress")
    -        writer = asyncio.create_task(asyncio.to_thread(submit_until_stopped))
    -        try:
    -            await asyncio.to_thread(started.wait)
    -            with (source_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -                handle.write("\n# concurrent clone fixture revision\n")
    -            candidate = await manager.prepare_candidate("content_clock_source")
    -            assert candidate is not None and candidate.runtime_snapshot is not None
    -            candidate_root = candidate.runtime_snapshot.composition_root
    -            assert candidate_root is not None
    -            candidate_runtime = candidate_root.plugin_runtime("eventmail")
    -            formal_path = (
    -                workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -            )
    -            assert candidate_runtime.data_dir / "eventmail.sqlite3" != formal_path
    -            candidate_wake = candidate_root.context.require(EVENTMAIL_WAKE)
    -            candidate_snapshot = cast(dict[str, Any], candidate_wake.snapshot(now))
    -            candidate_count = len(candidate_snapshot["items"])
    -            assert candidate_count >= 1
    -            assert (
    -                candidate_wake.selection(
    -                    {"session_id": "wake:candidate", "turn_id": "turn:missing"}
    -                )
    -                is None
    -            )
    -            candidate_source = candidate_root.context.require(
    -                EVENTMAIL_CONTENT_SOURCE
    -            ).bind("concurrent-candidate-probe")
    -            candidate_source.submit("poll:candidate-only", ())
    -        finally:
    -            stop.set()
    -            await writer
    -
    -        assert candidate_count <= written
    -        formal_store = EventMailStore(
    -            workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -        )
    -        assert sum(formal_store.state_counts().values()) == written
    -    finally:
    -        stop.set()
    -        if writer is not None and not writer.done():
    -            await writer
    -        try:
    -            if candidate is not None:
    -                await manager.discard_prepared("content_clock_source")
    -        finally:
    -            await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_readiness_rejects_unknown_content_schema(
    -    tmp_path: Path,
    -) -> None:
    -    content_dir, source_dir, probe_dir = _copy_plugins(tmp_path)
    -    workspace = tmp_path / "workspace"
    -    manager = PluginManager(
    -        plugin_dirs=[content_dir, source_dir, probe_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    content_path = workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -    connection = sqlite3.connect(content_path)
    -    connection.execute("PRAGMA user_version = 99")
    -    connection.commit()
    -    connection.close()
    -    try:
    -        with (source_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -            handle.write("\n# invalid clone fixture revision\n")
    -
    -        assert await manager.prepare_candidate("content_clock_source") is None
    -        assert manager.current_snapshot is not None
    -    finally:
    -        connection = sqlite3.connect(content_path)
    -        connection.execute("PRAGMA user_version = 1")
    -        connection.close()
    -        await manager.terminate_all()
    -
    -
    -def test_fixture_declares_its_own_structural_content_protocol() -> None:
    -    root = Path(__file__).resolve().parents[1]
    -    source = (
    -        root / "tests" / "fixtures" / "content_clock_source" / "plugin.py"
    -    ).read_text(encoding="utf-8")
    -    probe = (
    -        root / "tests" / "fixtures" / "content_hint_probe" / "plugin.py"
    -    ).read_text(encoding="utf-8")
    -
    -    assert "from plugins.eventmail" not in source
    -    assert 'ServiceKey[ContentSourceServices]("eventmail.content_source.v1")' in source
    -    assert "EVENTMAIL_WAKE" not in source
    -    assert "EVENTMAIL_CHANGED" not in source
    -    assert "SCOPED_TURNS" not in source
    -    assert "DELIVERIES" not in source
    -    assert "MCP_SERVERS" not in source
    -    assert "from plugins.eventmail" not in probe
    -    assert 'ServiceKey[ContentWakeServices]("eventmail.wake.v1")' in probe
    -    assert 'EmitEventKey[None]("eventmail.changed")' in probe
    -    assert "EVENTMAIL_CONTENT_SOURCE" not in probe
    -    assert "TIMERS" not in probe
    -    assert "SCOPED_TURNS" not in probe
    diff --git a/tests/test_context_compaction_config_contract.py b/tests/test_context_compaction_config_contract.py
    deleted file mode 100644
    index 1dd18f229..000000000
    --- a/tests/test_context_compaction_config_contract.py
    +++ /dev/null
    @@ -1,125 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -from typing import Any
    -
    -import pytest
    -
    -from agent.config import load_config
    -from plugins.compaction.engine import hard_input_limit
    -from plugins.compaction.plugin import Config as CompactionConfig
    -from tests.model_plugin_fakes import BoundChatModelFake
    -
    -
    -def test_integrations_peer_agents_is_rejected_at_config_boundary(
    -    tmp_path: Path,
    -) -> None:
    -    path = tmp_path / "config.toml"
    -    path.write_text(
    -        """
    -[agent.context.compaction]
    -keep_recent_tokens = 20000
    -
    -[integrations]
    -peer_agents = {}
    -""",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="integrations.peer_agents"):
    -        load_config(path, workspace=tmp_path)
    -
    -
    -def _runtime_config(*, extra: str = "") -> str:
    -    return f"""
    -[agent.context]
    -{extra}
    -[agent.context.compaction]
    -keep_recent_tokens = 21000
    -"""
    -
    -
    -class _BudgetProvider:
    -    """Minimal concrete provider seam for hard input boundary tests."""
    -
    -    context_window: int = 0
    -
    -    def __init__(self, context_window: int) -> None:
    -        self.context_window = context_window
    -
    -    async def chat(self, **kwargs: Any):
    -        raise AssertionError("budget fixture must not call provider.chat")
    -
    -    @property
    -    def descriptor(self):
    -        return BoundChatModelFake(self).descriptor
    -
    -
    -def test_compaction_policy_is_rejected_from_core_config_boundary(
    -    tmp_path: Path,
    -) -> None:
    -    path = tmp_path / "config.toml"
    -    path.write_text(_runtime_config(), encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match="plugin-data/compaction-builtin/config.local.toml"):
    -        load_config(path, workspace=tmp_path)
    -
    -
    -@pytest.mark.parametrize("raw", ["true", "false", "1.5", '"20000"'])
    -def test_core_config_rejects_retired_compaction_policy(
    -    tmp_path: Path,
    -    raw: str,
    -) -> None:
    -    path = tmp_path / "config.toml"
    -    path.write_text(
    -        _runtime_config().replace(
    -            "keep_recent_tokens = 21000", f"keep_recent_tokens = {raw}"
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="agent.context.compaction 已移除"):
    -        load_config(path, workspace=tmp_path)
    -
    -
    -@pytest.mark.parametrize("raw", [True, False, 1.5, "20000", 0, -1])
    -def test_compaction_config_rejects_invalid_direct_values(raw: object) -> None:
    -    with pytest.raises(ValueError, match="keep_recent_tokens"):
    -        CompactionConfig.model_validate({"keep_recent_tokens": raw})
    -
    -
    -@pytest.mark.parametrize(
    -    "extra",
    -    [
    -        "memory_window = 12\n",
    -    ],
    -)
    -def test_legacy_context_keys_fail_at_config_boundary(
    -    tmp_path: Path,
    -    extra: str,
    -) -> None:
    -    path = tmp_path / "config.toml"
    -    path.write_text(_runtime_config(extra=extra), encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match="removed configuration"):
    -        load_config(path, workspace=tmp_path)
    -
    -
    -def test_removed_agent_compaction_trigger_fails_at_config_boundary(
    -    tmp_path: Path,
    -) -> None:
    -    path = tmp_path / "config.toml"
    -    text = _runtime_config().replace(
    -        "[agent.context.compaction]\n",
    -        "[agent.context.compaction]\ntrigger_percent = 0.7\n",
    -    )
    -    path.write_text(text, encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match="agent.context.compaction.trigger_percent"):
    -        load_config(path, workspace=tmp_path)
    -
    -
    -def test_model_runtime_input_limit_does_not_subtract_output_budget() -> None:
    -    provider = _BudgetProvider(1025)
    -    assert hard_input_limit(BoundChatModelFake(provider), 1024) == 1025
    -    assert hard_input_limit(BoundChatModelFake(_BudgetProvider(1024)), 1024) == 1024
    diff --git a/tests/test_context_probe.py b/tests/test_context_probe.py
    deleted file mode 100644
    index 038beed1c..000000000
    --- a/tests/test_context_probe.py
    +++ /dev/null
    @@ -1,39 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -
    -import pytest
    -
    -from docker.debug.context_probe import _ensure_successful_reply, _send_and_read
    -
    -
    -class _Handle:
    -    async def result(self) -> dict[str, object]:
    -        return {"finalResponse": "formal reply"}
    -
    -
    -class _Client:
    -    def __init__(self) -> None:
    -        self.request: tuple[str, str] | None = None
    -
    -    async def start_turn(self, thread_id: str, text: str) -> _Handle:
    -        self.request = (thread_id, text)
    -        return _Handle()
    -
    -
    -def test_context_probe_rejects_runtime_failure_reply() -> None:
    -    with pytest.raises(RuntimeError, match="turn 3 返回运行时失败回复"):
    -        _ensure_successful_reply("处理消息时出错,请稍后再试。", 3)
    -
    -
    -def test_context_probe_accepts_normal_reply() -> None:
    -    _ensure_successful_reply("记住了,你喝茶不加糖。", 1)
    -
    -
    -def test_context_probe_uses_formal_turn_rpc() -> None:
    -    client = _Client()
    -
    -    reply = asyncio.run(_send_and_read(client, "thread-1", "hello", 3))  # type: ignore[arg-type]
    -
    -    assert reply == "formal reply"
    -    assert client.request == ("thread-1", "hello")
    diff --git a/tests/test_conversation_semantic_interest.py b/tests/test_conversation_semantic_interest.py
    deleted file mode 100644
    index 89d3a191a..000000000
    --- a/tests/test_conversation_semantic_interest.py
    +++ /dev/null
    @@ -1,114 +0,0 @@
    -from __future__ import annotations
    -
    -import sqlite3
    -from contextlib import closing
    -from datetime import UTC, datetime, timedelta
    -
    -import pytest
    -
    -from agent.plugin_composition.semantic_interest import ConversationSemanticInterest
    -from session.embedding_store import MessageEmbeddingStore
    -
    -
    -class _EmbeddingApi:
    -    model_id = "fixture-embedding"
    -
    -    async def embed_batch(self, texts: list[str]) -> list[list[float]]:
    -        vectors = {
    -            "looks proactive": [1.0, 0.0],
    -            "looks passive": [0.0, 1.0],
    -        }
    -        return [vectors[text] for text in texts]
    -
    -
    -@pytest.mark.asyncio
    -async def test_scores_against_passive_turns_and_ignores_twenty_proactive_rows(
    -    tmp_path,
    -) -> None:
    -    db_path = tmp_path / "sessions.db"
    -    now = datetime(2026, 8, 25, 1, tzinfo=UTC)
    -    with closing(sqlite3.connect(db_path)) as connection, connection:
    -        connection.execute("""
    -            CREATE TABLE messages(
    -                id TEXT PRIMARY KEY, session_key TEXT, seq INTEGER, role TEXT,
    -                content TEXT, extra TEXT, ts TEXT
    -            )
    -            """)
    -        rows = [
    -            ("u-pro", "mobile", 1, "user", "proactive seed", "{}", now.isoformat()),
    -            *[
    -                (
    -                    f"p{index}",
    -                    "mobile",
    -                    index + 2,
    -                    "assistant",
    -                    f"push {index}",
    -                    '{"proactive":true}',
    -                    (now + timedelta(seconds=index + 1)).isoformat(),
    -                )
    -                for index in range(20)
    -            ],
    -            (
    -                "u-passive",
    -                "mobile",
    -                22,
    -                "user",
    -                "passive user",
    -                "{}",
    -                (now + timedelta(seconds=30)).isoformat(),
    -            ),
    -            (
    -                "a-passive",
    -                "mobile",
    -                23,
    -                "assistant",
    -                "passive assistant",
    -                "{}",
    -                (now + timedelta(seconds=31)).isoformat(),
    -            ),
    -        ]
    -        connection.executemany("INSERT INTO messages VALUES(?, ?, ?, ?, ?, ?, ?)", rows)
    -    embeddings = MessageEmbeddingStore(db_path)
    -    embeddings.upsert(
    -        message_id="u-pro",
    -        content="proactive seed",
    -        model=_EmbeddingApi.model_id,
    -        embedding=[1.0, 0.0],
    -    )
    -    for index in range(20):
    -        embeddings.upsert(
    -            message_id=f"p{index}",
    -            content=f"push {index}",
    -            model=_EmbeddingApi.model_id,
    -            embedding=[1.0, 0.0],
    -        )
    -    embeddings.upsert(
    -        message_id="u-passive",
    -        content="passive user",
    -        model=_EmbeddingApi.model_id,
    -        embedding=[0.0, 1.0],
    -    )
    -    embeddings.upsert(
    -        message_id="a-passive",
    -        content="passive assistant",
    -        model=_EmbeddingApi.model_id,
    -        embedding=[0.0, 1.0],
    -    )
    -    embeddings.close()
    -
    -    service = ConversationSemanticInterest(db_path, _EmbeddingApi())
    -    scores = await service.score(
    -        ("looks proactive", "looks passive"),
    -        cutoff=(now + timedelta(minutes=1)).isoformat(),
    -    )
    -
    -    assert scores[0] == 0.0
    -    assert scores[1] == pytest.approx(0.999)
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_validation_cannot_read_formal_semantics() -> None:
    -    service = ConversationSemanticInterest.candidate_validation()
    -
    -    with pytest.raises(RuntimeError, match="candidate 验证期禁止"):
    -        await service.score(("candidate",), cutoff=datetime.now(UTC).isoformat())
    diff --git a/tests/test_core_channel_adapter.py b/tests/test_core_channel_adapter.py
    deleted file mode 100644
    index 2218ed188..000000000
    --- a/tests/test_core_channel_adapter.py
    +++ /dev/null
    @@ -1,289 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import json
    -from contextlib import suppress
    -
    -import pytest
    -
    -from agent.plugin_composition.channels import (
    -    ChannelCapability,
    -    ChannelFactoryContext,
    -    ChannelReady,
    -    ChannelRuntimePorts,
    -    DeliveryStatus,
    -    InboundIdentity,
    -    ProviderDeliveryReceipt,
    -    ProviderDeliveryRequest,
    -    StopReceipt,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.tools.message_push import MessagePushTool
    -from bootstrap.core_channel_adapter import build_core_channel_definition
    -from bootstrap.tools import _dispatch_v3_channel_push
    -from bus.event_bus import EventBus
    -from bus.queue import MessageBus
    -from session.manager import SessionManager
    -
    -
    -class _NativeAdapter:
    -    def __init__(
    -        self,
    -        context: ChannelFactoryContext,
    -        received: list[ProviderDeliveryRequest],
    -    ) -> None:
    -        self._binding_token = context.binding_token
    -        self._received = received
    -
    -    async def start(self) -> ChannelReady:
    -        return ChannelReady(self._binding_token)
    -
    -    async def deliver(
    -        self,
    -        request: ProviderDeliveryRequest,
    -    ) -> ProviderDeliveryReceipt:
    -        self._received.append(request)
    -        return ProviderDeliveryReceipt(request.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    async def stop(self) -> StopReceipt:
    -        return StopReceipt(self._binding_token, resources_closed=True)
    -
    -
    -class _NativeChannel:
    -    name = "web"
    -
    -    def __init__(self) -> None:
    -        self.received: list[ProviderDeliveryRequest] = []
    -        self.contexts: list[ChannelFactoryContext] = []
    -
    -    def build_v3_adapter(self, context: ChannelFactoryContext) -> _NativeAdapter:
    -        self.contexts.append(context)
    -        return _NativeAdapter(context, self.received)
    -
    -
    -class _AkashicNativeChannel(_NativeChannel):
    -    name = "akashic"
    -
    -
    -class _InboundNativeAdapter(_NativeAdapter):
    -    def __init__(
    -        self,
    -        context: ChannelFactoryContext,
    -        received: list[ProviderDeliveryRequest],
    -    ) -> None:
    -        super().__init__(context, received)
    -        self.runtime: ChannelRuntimePorts | None = None
    -        self.open = False
    -
    -    def attach_runtime(self, ports: ChannelRuntimePorts) -> None:
    -        self.runtime = ports
    -
    -    def open_admission(self) -> None:
    -        self.open = True
    -
    -    def close_admission(self) -> None:
    -        self.open = False
    -
    -
    -class _InboundNativeChannel(_NativeChannel):
    -    name = "telegram"
    -    v3_inbound_identity = InboundIdentity.PROVIDER_MESSAGE_ID
    -
    -    def __init__(self) -> None:
    -        super().__init__()
    -        self.adapter: _InboundNativeAdapter | None = None
    -
    -    def build_v3_adapter(self, context: ChannelFactoryContext) -> _InboundNativeAdapter:
    -        self.contexts.append(context)
    -        self.adapter = _InboundNativeAdapter(context, self.received)
    -        return self.adapter
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_publishes_native_core_catalog_without_plugins(tmp_path) -> None:
    -    """A Core channel is materialized only through its native v3 factory."""
    -
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    channel = _NativeChannel()
    -
    -    await manager.bind_core_channel_definitions(
    -        (build_core_channel_definition(channel),)
    -    )
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    runtime = manager.channel_generation_host.get(snapshot.snapshot_id)
    -    assert snapshot.channel_catalog is not None
    -    assert snapshot.channel_catalog.definition("web") is not None
    -    assert runtime is not None
    -    assert runtime.snapshot_id == snapshot.snapshot_id
    -    assert runtime.channel("web").admission_open is True
    -    assert len(channel.contexts) == 1
    -    assert channel.contexts[0].binding_token == runtime.channel("web").binding_token
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_native_inbound_definition_attaches_before_opening_provider_callbacks(
    -    tmp_path,
    -) -> None:
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    channel = _InboundNativeChannel()
    -
    -    await manager.bind_core_channel_definitions(
    -        (build_core_channel_definition(channel),)
    -    )
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    catalog = snapshot.channel_catalog
    -    assert catalog is not None
    -    definition = catalog.definition("telegram")
    -    assert definition is not None
    -    assert definition.capabilities == frozenset(
    -        {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -    )
    -    assert definition.inbound_identity is InboundIdentity.PROVIDER_MESSAGE_ID
    -    assert channel.adapter is not None
    -    assert channel.adapter.runtime is not None
    -    assert channel.adapter.open is True
    -
    -    await manager.terminate_all()
    -    assert channel.adapter.open is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_native_core_catalog_routes_message_push_without_legacy_fallback(
    -    tmp_path,
    -) -> None:
    -    """MessagePush reaches the native adapter with one exact committed request."""
    -
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    channel = _NativeChannel()
    -    await manager.bind_core_channel_definitions(
    -        (build_core_channel_definition(channel),)
    -    )
    -
    -    bus = MessageBus()
    -    bus.bind_channel_outbound_dispatcher(
    -        manager.channel_generation_host.dispatch_outbound
    -    )
    -    dispatch_task = asyncio.create_task(bus.dispatch_outbound())
    -    tool = MessagePushTool(chat_lane=bus.chat_lane)
    -    tool.bind_v3_channel_dispatcher(
    -        lambda message, passive: _dispatch_v3_channel_push(
    -            manager,
    -            bus,
    -            message,
    -            passive,
    -        )
    -    )
    -
    -    try:
    -        result = json.loads(
    -            await tool.execute(
    -                target_channel="web",
    -                target_chat_id="chat-1",
    -                message="report",
    -            )
    -        )
    -    finally:
    -        await bus.aclose()
    -        if not dispatch_task.done():
    -            dispatch_task.cancel()
    -        with suppress(asyncio.CancelledError):
    -            await dispatch_task
    -        await manager.terminate_all()
    -
    -    assert result["status"] == "delivered"
    -    assert result["retryable"] is False
    -    assert len(channel.received) == 1
    -    request = channel.received[0]
    -    assert request.recipient == "chat-1"
    -    assert request.body == "report"
    -    assert request.binding_token == channel.contexts[0].binding_token
    -
    -
    -@pytest.mark.asyncio
    -async def test_akashic_direct_push_commits_session_before_client_notification(
    -    tmp_path,
    -) -> None:
    -    sessions = SessionManager(tmp_path / "workspace")
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    channel = _AkashicNativeChannel()
    -    await manager.bind_core_channel_definitions(
    -        (build_core_channel_definition(channel),)
    -    )
    -    bus = MessageBus()
    -    bus.bind_channel_outbound_dispatcher(
    -        manager.channel_generation_host.dispatch_outbound
    -    )
    -    dispatch_task = asyncio.create_task(bus.dispatch_outbound())
    -    tool = MessagePushTool(chat_lane=bus.chat_lane)
    -    tool.bind_v3_channel_dispatcher(
    -        lambda message, passive: _dispatch_v3_channel_push(
    -            manager,
    -            bus,
    -            message,
    -            passive,
    -            session_manager=sessions,
    -        )
    -    )
    -
    -    try:
    -        result = json.loads(
    -            await tool.execute(
    -                target_channel="akashic",
    -                target_chat_id="chat-1",
    -                message="scheduled result",
    -            )
    -        )
    -    finally:
    -        await bus.aclose()
    -        if not dispatch_task.done():
    -            dispatch_task.cancel()
    -        with suppress(asyncio.CancelledError):
    -            await dispatch_task
    -        await manager.terminate_all()
    -
    -    messages = sessions.control_store.fetch_session_messages("akashic:chat-1")
    -    assert result["status"] == "delivered"
    -    assert len(messages) == 1
    -    assert messages[0]["id"] == "akashic:chat-1:0"
    -    assert messages[0]["seq"] == 0
    -    assert messages[0]["content"] == "scheduled result"
    -    assert messages[0]["effects"] == {"post_commit": "suppress"}
    -    assert channel.received[0].session_message_id == messages[0]["id"]
    -    sessions.close()
    -
    -
    -def test_core_catalog_rejects_channel_without_native_v3_factory() -> None:
    -    class _LegacyOnly:
    -        name = "legacy"
    -
    -        async def _deliver_message(self, _message: object) -> object:
    -            return object()
    -
    -    with pytest.raises(TypeError, match="build_v3_adapter"):
    -        build_core_channel_definition(_LegacyOnly())
    diff --git a/tests/test_core_channel_catalog.py b/tests/test_core_channel_catalog.py
    deleted file mode 100644
    index d3ab1bc39..000000000
    --- a/tests/test_core_channel_catalog.py
    +++ /dev/null
    @@ -1,191 +0,0 @@
    -from __future__ import annotations
    -
    -import pytest
    -
    -from agent.plugin_composition.channels import (
    -    ChannelCapability,
    -    ChannelDescriptor,
    -    ChannelFactoryProvenance,
    -    ChannelReady,
    -    ChannelRegistrySnapshot,
    -    CommittedChannelCatalog,
    -    CoreChannelDefinition,
    -    DeliveryStatus,
    -    InboundIdentity,
    -    ProviderDeliveryReceipt,
    -    ProviderDeliveryRequest,
    -    StopReceipt,
    -    _registry_identity,
    -)
    -
    -
    -class _RecordingAdapter:
    -    def __init__(self, binding_token: str = "core-binding") -> None:
    -        self.binding_token = binding_token
    -
    -    async def start(self) -> ChannelReady:
    -        return ChannelReady(self.binding_token)
    -
    -    async def deliver(self, request: ProviderDeliveryRequest) -> ProviderDeliveryReceipt:
    -        return ProviderDeliveryReceipt(request.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    async def stop(self) -> StopReceipt:
    -        return StopReceipt(self.binding_token, resources_closed=True)
    -
    -
    -def _build_adapter(_context: object) -> _RecordingAdapter:
    -    return _RecordingAdapter()
    -
    -
    -def _core_definition(name: str) -> CoreChannelDefinition:
    -    return CoreChannelDefinition(
    -        name=name,
    -        capabilities=frozenset({ChannelCapability.OUTBOUND}),
    -        factory=_build_adapter,
    -        inbound_identity=None,
    -        source_revision="core-source-1",
    -        config_revision="core-config-1",
    -        generation_id="core-generation-1",
    -        config={"channel": {"name": name}},
    -    )
    -
    -
    -def _plugin_registry(*, name: str = "feishu") -> ChannelRegistrySnapshot:
    -    descriptor = ChannelDescriptor(
    -        owner="plugin",
    -        name=name,
    -        capabilities=(ChannelCapability.OUTBOUND,),
    -        factory_export=f"{name}.build_channel",
    -        inbound_identity=None,
    -        credential_paths=("token",),
    -    )
    -    provenance = ChannelFactoryProvenance(
    -        plugin_id="plugin",
    -        generation_id="generation-1",
    -        channel_name=name,
    -        source_revision="source-1",
    -        config_revision="config-1",
    -        factory_export=descriptor.factory_export,
    -    )
    -    return ChannelRegistrySnapshot(
    -        descriptors=(descriptor,),
    -        factories=(provenance,),
    -        identity=_registry_identity((descriptor,), (provenance,)),
    -        root_instance_token=object(),
    -    )
    -
    -
    -def test_committed_catalog_merges_core_and_plugin_descriptors() -> None:
    -    plugin_registry = _plugin_registry()
    -    catalog = CommittedChannelCatalog(
    -        plugin_registry=plugin_registry,
    -        core_definitions=(_core_definition("telegram"), _core_definition("web")),
    -    )
    -
    -    assert tuple(item.name for item in catalog.descriptors) == (
    -        "feishu",
    -        "telegram",
    -        "web",
    -    )
    -    assert catalog.descriptors[1].owner == "core"
    -    assert catalog.definition("web") is not None
    -    assert catalog.definition("feishu") is None
    -    assert catalog.registry.root_instance_token is plugin_registry.root_instance_token
    -    assert catalog.identity == catalog.registry.identity
    -
    -
    -def test_committed_catalog_fails_loud_on_core_plugin_collision() -> None:
    -    with pytest.raises(ValueError, match="名称冲突: telegram"):
    -        CommittedChannelCatalog(
    -            plugin_registry=_plugin_registry(name="telegram"),
    -            core_definitions=(_core_definition("telegram"),),
    -        )
    -
    -
    -def test_committed_catalog_canonicalizes_core_definition_order() -> None:
    -    catalog = CommittedChannelCatalog(
    -        plugin_registry=_plugin_registry(),
    -        core_definitions=(_core_definition("web"), _core_definition("telegram")),
    -    )
    -
    -    assert tuple(item.name for item in catalog.core_definitions) == (
    -        "telegram",
    -        "web",
    -    )
    -
    -
    -def test_core_definition_validates_inbound_identity_and_freezes_config() -> None:
    -    with pytest.raises(ValueError, match="必须声明 inbound_identity"):
    -        CoreChannelDefinition(
    -            name="mobile",
    -            capabilities=frozenset({ChannelCapability.INBOUND}),
    -            factory=_build_adapter,
    -            inbound_identity=None,
    -            source_revision="core-source-1",
    -            config_revision="core-config-1",
    -            generation_id="core-generation-1",
    -        )
    -
    -    definition = CoreChannelDefinition(
    -        name="web",
    -        capabilities=frozenset({ChannelCapability.OUTBOUND}),
    -        factory=_build_adapter,
    -        inbound_identity=None,
    -        source_revision="core-source-1",
    -        config_revision="core-config-1",
    -        generation_id="core-generation-1",
    -        config={"nested": {"enabled": True}},
    -    )
    -    assert definition.config["nested"]["enabled"] is True  # type: ignore[index]
    -    with pytest.raises(TypeError):
    -        definition.config["nested"] = {}  # type: ignore[index]
    -
    -
    -def test_committed_catalog_can_start_from_core_only_definitions() -> None:
    -    root = object()
    -    catalog = CommittedChannelCatalog(
    -        core_definitions=(_core_definition("mobile"),),
    -        root_instance_token=root,
    -    )
    -
    -    assert catalog.plugin_registry is None
    -    assert catalog.root_instance_token is root
    -    assert catalog.registry.descriptors[0].owner == "core"
    -
    -
    -def test_core_config_changes_catalog_identity_but_input_order_does_not() -> None:
    -    first = _core_definition("telegram")
    -    changed = CoreChannelDefinition(
    -        name=first.name,
    -        capabilities=first.capabilities,
    -        factory=first.factory,
    -        inbound_identity=first.inbound_identity,
    -        source_revision=first.source_revision,
    -        config_revision=first.config_revision,
    -        generation_id=first.generation_id,
    -        config={"channel": {"name": "changed"}},
    -    )
    -    first_catalog = CommittedChannelCatalog(
    -        core_definitions=(first, _core_definition("web")),
    -    )
    -    reordered_catalog = CommittedChannelCatalog(
    -        core_definitions=(_core_definition("web"), first),
    -    )
    -    changed_catalog = CommittedChannelCatalog(
    -        core_definitions=(changed, _core_definition("web")),
    -    )
    -
    -    assert first_catalog.identity == reordered_catalog.identity
    -    assert first_catalog.identity != changed_catalog.identity
    -
    -
    -def test_plugin_channel_definition_still_requires_credentials() -> None:
    -    with pytest.raises(ValueError, match="非空 tuple"):
    -        ChannelDescriptor(
    -            owner="plugin",
    -            name="plugin_channel",
    -            capabilities=(ChannelCapability.OUTBOUND,),
    -            factory_export="plugin.build_channel",
    -            inbound_identity=None,
    -            credential_paths=(),
    -        )
    diff --git a/tests/test_drift_store.py b/tests/test_drift_store.py
    deleted file mode 100644
    index 515b3fee4..000000000
    --- a/tests/test_drift_store.py
    +++ /dev/null
    @@ -1,220 +0,0 @@
    -import sqlite3
    -from datetime import UTC, datetime, timedelta
    -
    -import pytest
    -
    -from plugins.drift.store import DriftStore
    -
    -
    -def test_drift_store_freezes_due_selects_and_transitions(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 8, tzinfo=UTC)
    -    store = DriftStore(tmp_path / "drift.sqlite3")
    -    store.initialize()
    -    proposed = store.propose(
    -        "reflection",
    -        "1",
    -        {"prompt": "想一想今天"},
    -        now,
    -        next_due=now + timedelta(minutes=5),
    -    )
    -    assert proposed == {
    -        "inserted": True,
    -        "ref": {
    -            "proposal_id": "reflection",
    -            "revision": "1",
    -            "state_version": 1,
    -        },
    -    }
    -
    -    snapshot = store.snapshot(now)
    -    proposal = snapshot["proposals"][0]
    -    receipt = store.select(
    -        proposal["ref"],
    -        {"session_id": "wake:default", "turn_id": "turn:1"},
    -        now,
    -    )
    -    assert receipt["selected"] is True
    -    selected = store.selected()
    -    assert selected[0]["accepted_turn"]["turn_id"] == "turn:1"
    -    result = store.transition(selected[0]["selection_token"], "defer")
    -    assert result == {
    -        "changed": True,
    -        "status": "deferred",
    -        "next_due": (now + timedelta(minutes=5)).isoformat(),
    -    }
    -
    -
    -def test_drift_store_cas_loser_cannot_select_same_revision(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 8, tzinfo=UTC)
    -    store = DriftStore(tmp_path / "drift.sqlite3")
    -    store.initialize()
    -    store.propose("reflection", "1", {}, now)
    -    proposal = store.snapshot(now)["proposals"][0]
    -
    -    first = store.select(
    -        proposal["ref"],
    -        {"session_id": "wake:default", "turn_id": "turn:1"},
    -        now,
    -    )
    -    second = store.select(
    -        proposal["ref"],
    -        {"session_id": "wake:default", "turn_id": "turn:2"},
    -        now,
    -    )
    -    assert first["selected"] is True
    -    assert second["selected"] is False
    -
    -
    -def test_drift_same_turn_second_proposal_is_explicit_cas_loser(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 8, tzinfo=UTC)
    -    store = DriftStore(tmp_path / "drift.sqlite3")
    -    store.initialize()
    -    store.propose("one", "1", {}, now)
    -    store.propose("two", "1", {}, now)
    -    first, second = store.snapshot(now)["proposals"]
    -    accepted = {"session_id": "wake:default", "turn_id": "turn:1"}
    -
    -    assert store.select(first["ref"], accepted, now)["selected"] is True
    -    loser = store.select(second["ref"], accepted, now)
    -
    -    assert loser == {
    -        "selected": False,
    -        "reason": "turn_already_selected",
    -        "selection_token": None,
    -        "accepted_turn": None,
    -    }
    -    assert store.selection(accepted)["ref"] == {
    -        "proposal_id": "one",
    -        "revision": "1",
    -    }
    -
    -
    -def test_drift_ready_delivery_preserves_turn_and_settles_once(tmp_path) -> None:
    -    now = datetime(2026, 8, 23, 8, tzinfo=UTC)
    -    store = DriftStore(tmp_path / "drift.sqlite3")
    -    store.initialize()
    -    store.propose("reflection", "1", {}, now)
    -    proposal = store.snapshot(now)["proposals"][0]
    -    accepted = {"session_id": "wake:default", "turn_id": "turn:share"}
    -    selected = store.select(proposal["ref"], accepted, now)
    -    token = selected["selection_token"]
    -    assert isinstance(token, str)
    -
    -    assert store.transition(token, "ready_for_delivery") == {
    -        "changed": True,
    -        "status": "ready_for_delivery",
    -        "next_due": None,
    -    }
    -    assert store.pending_delivery() == (
    -        {
    -            "selection_token": token,
    -            "accepted_turn": accepted,
    -            "message_metadata": {
    -                "tools_used": ["message_push"],
    -                "evidence_item_ids": [],
    -                "source_refs": [],
    -                "state_summary_tag": "none",
    -            },
    -        },
    -    )
    -    first = store.settle_delivery(token, "wake:delivery")
    -    second = store.settle_delivery(token, "wake:delivery")
    -
    -    assert first["settled"] is True and first["duplicate"] is False
    -    assert second["settled"] is True and second["duplicate"] is True
    -    assert first["receipt"] == second["receipt"]
    -    assert store.delivery(accepted)["status"] == "settled"
    -
    -
    -def test_drift_v1_orphaned_ready_row_is_invalidated_without_guessing_body(
    -    tmp_path,
    -) -> None:
    -    path = tmp_path / "drift.sqlite3"
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        CREATE TABLE proposals(
    -            proposal_id TEXT NOT NULL,
    -            revision TEXT NOT NULL,
    -            payload_json TEXT NOT NULL,
    -            status TEXT NOT NULL,
    -            due_at TEXT NOT NULL,
    -            next_due TEXT,
    -            state_version INTEGER NOT NULL,
    -            selection_token TEXT UNIQUE,
    -            selected_session_id TEXT,
    -            selected_turn_id TEXT,
    -            created_at TEXT NOT NULL,
    -            updated_at TEXT NOT NULL,
    -            PRIMARY KEY(proposal_id, revision)
    -        );
    -        CREATE UNIQUE INDEX proposals_selected_turn_idx
    -        ON proposals(selected_session_id, selected_turn_id)
    -        WHERE selected_turn_id IS NOT NULL;
    -        CREATE INDEX proposals_due_idx ON proposals(status, due_at);
    -        INSERT INTO proposals VALUES(
    -            'reflection', '1', '{}', 'ready_for_delivery',
    -            '2026-08-23T08:00:00+00:00', NULL, 2,
    -            NULL, NULL, NULL,
    -            '2026-08-23T08:00:00+00:00', '2026-08-23T08:01:00+00:00'
    -        );
    -        PRAGMA user_version = 1;
    -        """)
    -    connection.close()
    -
    -    store = DriftStore(path)
    -    store.initialize()
    -
    -    assert store.snapshot(datetime(2026, 8, 23, 9, tzinfo=UTC))["proposals"] == ()
    -    connection = sqlite3.connect(path)
    -    status, state_version = connection.execute(
    -        "SELECT status, state_version FROM proposals"
    -    ).fetchone()
    -    connection.close()
    -    assert (status, state_version) == ("invalidated", 3)
    -
    -
    -@pytest.mark.parametrize("mutation", ["missing_index", "extra_table"])
    -def test_drift_rejects_same_version_schema_topology_drift(tmp_path, mutation) -> None:
    -    path = tmp_path / "drift.sqlite3"
    -    store = DriftStore(path)
    -    store.initialize()
    -    connection = sqlite3.connect(path)
    -    if mutation == "missing_index":
    -        connection.execute("DROP INDEX proposals_due_idx")
    -    else:
    -        connection.execute("CREATE TABLE unexpected(value TEXT)")
    -    connection.commit()
    -    connection.close()
    -
    -    with pytest.raises(RuntimeError, match="Drift .*不匹配"):
    -        store.initialize()
    -
    -
    -def test_drift_rejects_constraint_free_same_version_table(tmp_path) -> None:
    -    path = tmp_path / "drift.sqlite3"
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        CREATE TABLE proposals(
    -            proposal_id TEXT NOT NULL,
    -            revision TEXT NOT NULL,
    -            payload_json TEXT NOT NULL,
    -            status TEXT NOT NULL,
    -            due_at TEXT NOT NULL,
    -            next_due TEXT,
    -            state_version INTEGER NOT NULL,
    -            selection_token TEXT,
    -            selected_session_id TEXT,
    -            selected_turn_id TEXT,
    -            created_at TEXT NOT NULL,
    -            updated_at TEXT NOT NULL
    -        );
    -        CREATE UNIQUE INDEX proposals_selected_turn_idx
    -        ON proposals(selected_session_id, selected_turn_id)
    -        WHERE selected_turn_id IS NOT NULL;
    -        CREATE INDEX proposals_due_idx ON proposals(status, due_at);
    -        PRAGMA user_version = 1;
    -        """)
    -    connection.close()
    -
    -    with pytest.raises(RuntimeError, match="constraint-bearing table SQL"):
    -        DriftStore(path).initialize()
    diff --git a/tests/test_fire_at.py b/tests/test_fire_at.py
    deleted file mode 100644
    index 51b15fe11..000000000
    --- a/tests/test_fire_at.py
    +++ /dev/null
    @@ -1,90 +0,0 @@
    -"""Tests for fire_at and actual_trigger computation."""
    -
    -from datetime import datetime, timedelta, timezone
    -
    -import pytest
    -
    -from agent.scheduler import (
    -    LatencyTracker,
    -    compute_actual_trigger,
    -    compute_fire_at,
    -)
    -
    -# Fixed "now" for deterministic tests
    -_NOW = datetime(2025, 6, 1, 12, 0, 0, tzinfo=timezone.utc)
    -_NOW_FN = lambda: _NOW  # noqa: E731
    -
    -
    -class TestComputeFireAt:
    -    def test_after_with_request_time(self):
    -        # request_time is T+0, tool called at T+20s — fire_at should be T+30s
    -        request_time = _NOW.isoformat()
    -        fire_at = compute_fire_at("after", "30s", "UTC", request_time, _NOW_FN)
    -        assert fire_at == _NOW + timedelta(seconds=30)
    -
    -    def test_after_compensates_for_tool_call_delay(self):
    -        # request_time is 20s before tool is called
    -        request_time = (_NOW - timedelta(seconds=20)).isoformat()
    -        now_fn = lambda: _NOW  # noqa: E731  (tool called at _NOW)
    -        fire_at = compute_fire_at("after", "30s", "UTC", request_time, now_fn)
    -        # fire_at = request_time + 30s = (_NOW - 20s) + 30s = _NOW + 10s
    -        assert fire_at == _NOW + timedelta(seconds=10)
    -
    -    def test_after_without_request_time_uses_now(self):
    -        fire_at = compute_fire_at("after", "5m", "UTC", None, _NOW_FN)
    -        assert fire_at == _NOW + timedelta(minutes=5)
    -
    -    def test_at_absolute_iso(self):
    -        fire_at = compute_fire_at("at", "2025-06-01T14:00:00", "UTC", None, _NOW_FN)
    -        assert fire_at.hour == 14
    -        assert fire_at.minute == 0
    -
    -    def test_every_interval_returns_now_plus_interval(self):
    -        fire_at = compute_fire_at("every", "1h", "UTC", None, _NOW_FN)
    -        assert fire_at == _NOW + timedelta(hours=1)
    -
    -    def test_after_request_time_with_tz_offset_not_treated_as_utc(self):
    -        # request_time 包含 +08:00 时,不能被当作 UTC 处理
    -        # CST 15:48:40 = UTC 07:48:40
    -        request_time = "2025-06-01T15:48:40+08:00"
    -        now_fn = lambda: datetime(2025, 6, 1, 7, 48, 50, tzinfo=timezone.utc)
    -        fire_at = compute_fire_at("after", "30s", "UTC", request_time, now_fn)
    -        # fire_at 应是 CST 15:49:10 = UTC 07:49:10,不是 UTC 15:49:10
    -        assert fire_at.utctimetuple().tm_hour == 7
    -        assert fire_at.utctimetuple().tm_min == 49
    -
    -    def test_unknown_trigger_raises(self):
    -        with pytest.raises(ValueError, match="未知触发类型"):
    -            compute_fire_at("sometime", "5m", "UTC", None, _NOW_FN)
    -
    -
    -class TestComputeActualTrigger:
    -    def test_instant_no_pretrigger(self):
    -        tracker = LatencyTracker(default=25.0)
    -        fire_at = _NOW + timedelta(minutes=5)
    -        actual = compute_actual_trigger(fire_at, "instant", tracker)
    -        assert actual == fire_at
    -
    -    def test_soft_subtracts_lead(self):
    -        tracker = LatencyTracker(default=30.0)
    -        fire_at = _NOW + timedelta(minutes=5)
    -        actual = compute_actual_trigger(fire_at, "soft", tracker)
    -        assert actual == fire_at - timedelta(seconds=30)
    -
    -    def test_soft_uses_adaptive_p90(self):
    -        tracker = LatencyTracker(default=25.0, window=20)
    -        for _ in range(20):
    -            tracker.record(10.0)  # stable 10s latency → P90 = 10s
    -        fire_at = _NOW + timedelta(minutes=5)
    -        actual = compute_actual_trigger(fire_at, "soft", tracker)
    -        # actual_trigger should be ≈ fire_at - 10s (not 25s default)
    -        diff = (fire_at - actual).total_seconds()
    -        assert 9 < diff < 12
    -
    -    def test_soft_fire_at_in_past_returns_past_time(self):
    -        # Scheduler should still fire it; the tick logic will catch it
    -        tracker = LatencyTracker(default=30.0)
    -        fire_at = _NOW - timedelta(seconds=5)  # already past
    -        actual = compute_actual_trigger(fire_at, "soft", tracker)
    -        # actual_trigger = fire_at - 30s, even more in the past — that's fine
    -        assert actual < _NOW
    diff --git a/tests/test_fresh_configuration_matrix.py b/tests/test_fresh_configuration_matrix.py
    deleted file mode 100644
    index d50e8c8fb..000000000
    --- a/tests/test_fresh_configuration_matrix.py
    +++ /dev/null
    @@ -1,64 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.config import Config
    -from bootstrap import app as bootstrap_app
    -from bootstrap.init_workspace import init_workspace
    -
    -
    -class _FakeServer:
    -    def __init__(self) -> None:
    -        self.should_exit = False
    -
    -    async def serve(self) -> None:
    -        while not self.should_exit:
    -            await asyncio.sleep(0)
    -
    -
    -def _prepare_fresh_case(root: Path) -> tuple[Path, Path, Path]:
    -    """Create one fresh workspace using the ordinary Akasha plugin default."""
    -
    -    home = root / "home"
    -    config_path = root / "config.toml"
    -    workspace = root / "workspace"
    -    home.mkdir()
    -    _ = init_workspace(config_path=config_path, workspace=workspace)
    -    return home, config_path, workspace
    -
    -
    -@pytest.mark.asyncio
    -async def test_fresh_init_runtime_start_stop_matrix(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    home = tmp_path / "home"
    -    monkeypatch.setattr(Path, "home", lambda: home)
    -    _home, config_path, workspace = _prepare_fresh_case(tmp_path)
    -    monkeypatch.setattr(
    -        bootstrap_app,
    -        "build_dashboard_server",
    -        lambda **_kwargs: _FakeServer(),
    -    )
    -    monkeypatch.setattr(
    -        bootstrap_app,
    -        "build_chat_server",
    -        lambda **_kwargs: _FakeServer(),
    -    )
    -    config = Config.load(config_path, workspace=workspace)
    -    runtime = bootstrap_app.AppRuntime(config, workspace)
    -
    -    try:
    -        await runtime.start()
    -        assert runtime.core is not None
    -        assert runtime.core.plugin_manager.current_snapshot is not None
    -        active = {
    -            item.plugin_id for item in runtime.core.plugin_manager.active_plugins()
    -        }
    -        assert "akasha" in active
    -        assert "default_memory" not in active
    -    finally:
    -        await runtime.shutdown()
    diff --git a/tests/test_host_runtime_cli.py b/tests/test_host_runtime_cli.py
    deleted file mode 100644
    index 93e3eb2b1..000000000
    --- a/tests/test_host_runtime_cli.py
    +++ /dev/null
    @@ -1,79 +0,0 @@
    -from __future__ import annotations
    -
    -import os
    -import subprocess
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.core.passive_turn import _host_runtime_execution_hint
    -from agent.host_bridge.server import _materialize_runtime_cli
    -
    -
    -def test_runtime_cli_is_bound_to_materialized_release(tmp_path: Path) -> None:
    -    checkout = tmp_path / "checkout"
    -    checkout.mkdir()
    -    (checkout / "main.py").write_text("# runtime entry\n", encoding="utf-8")
    -    fake_python = tmp_path / "bridge-python"
    -    fake_python.write_text(
    -        '#!/bin/sh\nprintf \'%s\\n\' "$PYTHONPATH" "$@"\n',
    -        encoding="utf-8",
    -    )
    -    fake_python.chmod(0o755)
    -    bridge_python = tmp_path / "bridge-venv" / "bin" / "python"
    -    bridge_python.parent.mkdir(parents=True)
    -    bridge_python.symlink_to(fake_python)
    -    stale = (
    -        tmp_path
    -        / "artifacts"
    -        / "runtime-cli"
    -        / ("a" * 40)
    -        / f".akashic-runtime.{os.getpid()}.tmp"
    -    )
    -    stale.parent.mkdir(parents=True)
    -    stale.write_text("stale", encoding="utf-8")
    -    stale.chmod(0o500)
    -    launcher = _materialize_runtime_cli(
    -        tmp_path / "artifacts",
    -        checkout,
    -        bridge_python,
    -        "a" * 40,
    -    )
    -
    -    result = subprocess.run(
    -        [str(launcher), "plugin-doctor", "demo@github"],
    -        check=True,
    -        capture_output=True,
    -        text=True,
    -        env={
    -            **os.environ,
    -            "AKASHIC_BRIDGE_PYTHON": "/attacker/python",
    -            "AKASHIC_RUNTIME_CHECKOUT": "/attacker/checkout",
    -        },
    -    )
    -
    -    assert result.stdout.splitlines() == [
    -        str(checkout),
    -        str(checkout / "main.py"),
    -        "plugin-doctor",
    -        "demo@github",
    -    ]
    -    assert str(bridge_python) in launcher.read_text(encoding="utf-8")
    -    assert str(fake_python) not in launcher.read_text(encoding="utf-8")
    -
    -
    -def test_host_runtime_hint_is_explicit_and_local_mode_is_silent(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_EXECUTION_MODE", "host-bridge")
    -    monkeypatch.setenv("AKASHIC_RUNTIME_COMMIT", "a" * 40)
    -    monkeypatch.setenv("AKASHIC_RUNTIME_CHECKOUT", "/srv/runtime")
    -
    -    hint = _host_runtime_execution_hint()
    -
    -    assert "a" * 40 in hint
    -    assert "/srv/runtime" in hint
    -    assert "akashic-runtime" in hint
    -    assert "不要用 host 的 python" in hint
    -    monkeypatch.setenv("AKASHIC_EXECUTION_MODE", "local")
    -    assert _host_runtime_execution_hint() == ""
    diff --git a/tests/test_host_runtime_healthcheck.py b/tests/test_host_runtime_healthcheck.py
    deleted file mode 100644
    index d0967df2a..000000000
    --- a/tests/test_host_runtime_healthcheck.py
    +++ /dev/null
    @@ -1,107 +0,0 @@
    -from __future__ import annotations
    -
    -import importlib.util
    -import json
    -from pathlib import Path
    -
    -import pytest
    -
    -
    -def _load_healthcheck():
    -    path = Path(__file__).parents[1] / "docker/host-runtime/healthcheck.py"
    -    spec = importlib.util.spec_from_file_location("host_runtime_healthcheck", path)
    -    assert spec is not None and spec.loader is not None
    -    module = importlib.util.module_from_spec(spec)
    -    spec.loader.exec_module(module)
    -    return module
    -
    -
    -class _HealthResponse:
    -    status = 200
    -
    -    def __enter__(self):
    -        return self
    -
    -    def __exit__(self, *_args: object) -> None:
    -        return None
    -
    -    def read(self) -> bytes:
    -        return b'{"status":"ready"}'
    -
    -
    -class _NotReadyHealthResponse(_HealthResponse):
    -    def read(self) -> bytes:
    -        return b'{"status":"ok"}'
    -
    -
    -def test_healthcheck_requires_identity_and_web_route(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    module = _load_healthcheck()
    -    readiness = {
    -        "bootId": "boot-1",
    -        "pid": 123,
    -        "state": "ready",
    -        "sourceCommit": "a" * 40,
    -        "hostCheckout": "/srv/runtime",
    -    }
    -    (tmp_path / ".runtime-ready.json").write_text(json.dumps(readiness))
    -    monkeypatch.setenv("AKASHIC_WORKSPACE", str(tmp_path))
    -    monkeypatch.setenv("AKASHIC_RUNTIME_COMMIT", "a" * 40)
    -    monkeypatch.setenv("AKASHIC_RUNTIME_CHECKOUT", "/srv/runtime")
    -    killed: list[tuple[int, int]] = []
    -    monkeypatch.setattr(module.os, "kill", lambda pid, signal: killed.append((pid, signal)))
    -    monkeypatch.setattr(module.urllib.request, "urlopen", lambda *_args, **_kwargs: _HealthResponse())
    -
    -    module.main()
    -
    -    assert killed == [(123, 0)]
    -
    -
    -def test_healthcheck_rejects_stale_commit(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    module = _load_healthcheck()
    -    readiness = {
    -        "bootId": "boot-1",
    -        "pid": 123,
    -        "state": "ready",
    -        "sourceCommit": "b" * 40,
    -        "hostCheckout": "/srv/runtime",
    -    }
    -    (tmp_path / ".runtime-ready.json").write_text(json.dumps(readiness))
    -    monkeypatch.setenv("AKASHIC_WORKSPACE", str(tmp_path))
    -    monkeypatch.setenv("AKASHIC_RUNTIME_COMMIT", "a" * 40)
    -    monkeypatch.setenv("AKASHIC_RUNTIME_CHECKOUT", "/srv/runtime")
    -
    -    with pytest.raises(RuntimeError, match="identity"):
    -        module.main()
    -
    -
    -def test_healthcheck_rejects_non_ready_web_route(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    module = _load_healthcheck()
    -    readiness = {
    -        "bootId": "boot-1",
    -        "pid": 123,
    -        "state": "ready",
    -        "sourceCommit": "a" * 40,
    -        "hostCheckout": "/srv/runtime",
    -    }
    -    (tmp_path / ".runtime-ready.json").write_text(json.dumps(readiness))
    -    monkeypatch.setenv("AKASHIC_WORKSPACE", str(tmp_path))
    -    monkeypatch.setenv("AKASHIC_RUNTIME_COMMIT", "a" * 40)
    -    monkeypatch.setenv("AKASHIC_RUNTIME_CHECKOUT", "/srv/runtime")
    -    monkeypatch.setattr(module.os, "kill", lambda *_args: None)
    -    monkeypatch.setattr(
    -        module.urllib.request,
    -        "urlopen",
    -        lambda *_args, **_kwargs: _NotReadyHealthResponse(),
    -    )
    -
    -    with pytest.raises(RuntimeError, match="payload"):
    -        module.main()
    diff --git a/tests/test_host_runtime_lifecycle.py b/tests/test_host_runtime_lifecycle.py
    deleted file mode 100644
    index 8259230ad..000000000
    --- a/tests/test_host_runtime_lifecycle.py
    +++ /dev/null
    @@ -1,142 +0,0 @@
    -from __future__ import annotations
    -
    -import re
    -import subprocess
    -from pathlib import Path
    -
    -from agent.tools.unified_exec import MAX_WRITE_STDIN_YIELD_TIME_MS
    -
    -ROOT = Path(__file__).resolve().parents[1]
    -SYSTEMD = ROOT / "docker" / "host-runtime" / "systemd"
    -
    -
    -def test_core_consumes_external_services_without_owning_them() -> None:
    -    core_unit = (SYSTEMD / "akashic-core.service").read_text(encoding="utf-8")
    -    network = (ROOT / "docker/host-runtime/compose.external-services.yaml").read_text(
    -        encoding="utf-8"
    -    )
    -
    -    assert "Requires=akashic-host-bridge.service" in core_unit
    -    assert "Wants=akashic-home-services.service" in core_unit
    -    assert (
    -        "After=docker.service akashic-host-bridge.service akashic-home-services.service"
    -        in core_unit
    -    )
    -    assert "PartOf=akashic-host-bridge.service" in core_unit
    -    assert "Requires=akashic-home-services.service" not in core_unit
    -    assert "PartOf=akashic-home-services.service" not in core_unit
    -    assert "home-services.env" not in core_unit
    -    assert "verify-running-home-services" not in core_unit
    -    assert "compose.external-services.yaml" in core_unit
    -    assert "external: true" in network
    -    assert not (ROOT / "docker/home-services").exists()
    -    assert not (SYSTEMD / "akashic-home-services.service").exists()
    -    assert not (SYSTEMD / "akashic-opencli-browser.service").exists()
    -
    -
    -def test_core_and_dynamic_workloads_share_a_compose_owned_network() -> None:
    -    compose = (ROOT / "docker/host-runtime/compose.experiment.yaml").read_text(
    -        encoding="utf-8"
    -    )
    -
    -    assert "AKASHIC_WORKLOAD_NETWORK" in compose
    -    assert "- akashic-workloads" in compose
    -    assert 'name: "${AKASHIC_WORKLOAD_NETWORK:-akashic-workloads}"' in compose
    -    assert "external: true" not in compose
    -    assert "--workload-uid" in compose
    -    assert "--workload-gid" in compose
    -
    -
    -def test_host_bridge_lease_covers_longest_write_stdin_wait() -> None:
    -    bridge_unit = (SYSTEMD / "akashic-host-bridge.service").read_text(encoding="utf-8")
    -    match = re.search(r"--lease-timeout (\d+)", bridge_unit)
    -
    -    assert match is not None
    -    assert int(match.group(1)) > MAX_WRITE_STDIN_YIELD_TIME_MS / 1_000 + 2
    -
    -
    -def test_release_restart_does_not_control_external_services(monkeypatch) -> None:
    -    from scripts.restart_host_runtime_release import run_release_restart
    -
    -    calls: list[list[str]] = []
    -
    -    def run(
    -        arguments: list[str], **_kwargs: object
    -    ) -> subprocess.CompletedProcess[str]:
    -        calls.append(arguments)
    -        output = "running|healthy\n" if arguments[0] == "docker" else ""
    -        return subprocess.CompletedProcess(arguments, 0, output, "")
    -
    -    monkeypatch.setattr("subprocess.run", run)
    -    run_release_restart("akashic-core")
    -
    -    assert calls[:3] == [
    -        ["systemctl", "stop", "akashic-core.service", "akashic-host-bridge.service"],
    -        ["systemctl", "start", "akashic-host-bridge.service"],
    -        ["systemctl", "start", "akashic-core.service"],
    -    ]
    -    assert calls[3:5] == [
    -        ["systemctl", "is-active", "--quiet", unit]
    -        for unit in ("akashic-host-bridge.service", "akashic-core.service")
    -    ]
    -    assert all("akashic-home-services.service" not in call for call in calls)
    -    assert all("akashic-opencli-browser.service" not in call for call in calls)
    -
    -
    -def test_release_restart_rejects_unhealthy_core(monkeypatch) -> None:
    -    import pytest
    -
    -    from scripts.restart_host_runtime_release import wait_for_core_health
    -
    -    monkeypatch.setattr(
    -        "subprocess.run",
    -        lambda arguments, **_kwargs: subprocess.CompletedProcess(
    -            arguments, 0, "running|unhealthy\n", ""
    -        ),
    -    )
    -    with pytest.raises(RuntimeError, match="healthcheck 未通过"):
    -        wait_for_core_health("akashic-core", 10)
    -
    -
    -def test_release_restart_waits_for_container_creation(monkeypatch) -> None:
    -    from scripts.restart_host_runtime_release import wait_for_core_health
    -
    -    results = iter(
    -        (
    -            subprocess.CompletedProcess(
    -                [], 1, "", "Error: No such object: akashic-core"
    -            ),
    -            subprocess.CompletedProcess([], 0, "running|healthy\n", ""),
    -        )
    -    )
    -    monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: next(results))
    -    monkeypatch.setattr("time.sleep", lambda _seconds: None)
    -    wait_for_core_health("akashic-core", 10)
    -
    -
    -def test_release_restart_waits_while_container_depends_on_controller(
    -    monkeypatch,
    -) -> None:
    -    from scripts.restart_host_runtime_release import wait_for_core_health
    -
    -    results = iter(
    -        (
    -            subprocess.CompletedProcess([], 0, "created|missing\n", ""),
    -            subprocess.CompletedProcess([], 0, "running|starting\n", ""),
    -            subprocess.CompletedProcess([], 0, "running|healthy\n", ""),
    -        )
    -    )
    -    monkeypatch.setattr("subprocess.run", lambda *_args, **_kwargs: next(results))
    -    monkeypatch.setattr("time.sleep", lambda _seconds: None)
    -
    -    wait_for_core_health("akashic-core", 10)
    -
    -
    -def test_release_restart_reads_container_name_from_runtime_env(tmp_path: Path) -> None:
    -    from scripts.restart_host_runtime_release import runtime_container_name
    -
    -    environment = tmp_path / "runtime.env"
    -    environment.write_text(
    -        "AKASHIC_CONTAINER_NAME=akashic-core-canary\n", encoding="utf-8"
    -    )
    -    assert runtime_container_name(environment) == "akashic-core-canary"
    diff --git a/tests/test_host_toolchain_identity.py b/tests/test_host_toolchain_identity.py
    deleted file mode 100644
    index 67450852a..000000000
    --- a/tests/test_host_toolchain_identity.py
    +++ /dev/null
    @@ -1,82 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import subprocess
    -from pathlib import Path
    -
    -import pytest
    -
    -from scripts.host_toolchain_identity import (
    -    declared_toolchain_identity,
    -    resolve_toolchain_identity,
    -)
    -
    -
    -def _repository(tmp_path: Path) -> Path:
    -    repository = tmp_path / "repository"
    -    repository.mkdir()
    -    subprocess.run(["git", "init", "-q"], cwd=repository, check=True)
    -    subprocess.run(
    -        ["git", "config", "user.email", "rehearsal@example.invalid"],
    -        cwd=repository,
    -        check=True,
    -    )
    -    subprocess.run(
    -        ["git", "config", "user.name", "Rehearsal"], cwd=repository, check=True
    -    )
    -    (repository / "mise.toml").write_text(
    -        '[tools]\nnode="22.23.1"\nnpm="10.9.8"\npython="3.14.6"\n'
    -        'uv="0.12.3"\n"npm:@jackwener/opencli"="1.8.6"\n'
    -        'opencode="1.18.15"\n',
    -        encoding="utf-8",
    -    )
    -    subprocess.run(["git", "add", "."], cwd=repository, check=True)
    -    subprocess.run(["git", "commit", "-qm", "fixture"], cwd=repository, check=True)
    -    return repository
    -
    -
    -def _fake_mise(tmp_path: Path) -> Path:
    -    mise = tmp_path / "mise"
    -    versions = {
    -        "node": "v22.23.1",
    -        "npm": "10.9.8",
    -        "python": "Python 3.14.6",
    -        "uv": "uv 0.12.3",
    -        "opencli": "1.8.6",
    -        "opencode": "1.18.15",
    -    }
    -    mise.write_text(
    -        "#!/usr/bin/env python3\n"
    -        "import json, sys\n"
    -        f"versions = {json.dumps(versions)}\n"
    -        "print(versions[sys.argv[3]])\n",
    -        encoding="utf-8",
    -    )
    -    mise.chmod(0o755)
    -    return mise
    -
    -
    -def test_resolve_toolchain_identity_is_commit_bound(tmp_path: Path) -> None:
    -    repository = _repository(tmp_path)
    -    identity = resolve_toolchain_identity(repository, _fake_mise(tmp_path))
    -    assert (
    -        identity["releaseCommit"]
    -        == subprocess.run(
    -            ["git", "rev-parse", "HEAD"],
    -            cwd=repository,
    -            check=True,
    -            capture_output=True,
    -            text=True,
    -        ).stdout.strip()
    -    )
    -    assert len(str(identity["toolchainDigest"])) == 64
    -    assert identity == declared_toolchain_identity(
    -        str(identity["releaseCommit"]), (repository / "mise.toml").read_bytes()
    -    )
    -
    -
    -def test_resolve_toolchain_identity_rejects_dirty_checkout(tmp_path: Path) -> None:
    -    repository = _repository(tmp_path)
    -    (repository / "dirty.txt").write_text("dirty", encoding="utf-8")
    -    with pytest.raises(RuntimeError, match="clean"):
    -        resolve_toolchain_identity(repository, _fake_mise(tmp_path))
    diff --git a/tests/test_http_migrations.py b/tests/test_http_migrations.py
    index 914839eec..ac7eb0a45 100644
    --- a/tests/test_http_migrations.py
    +++ b/tests/test_http_migrations.py
    @@ -3,8 +3,6 @@
     import httpx
     import pytest
     
    -from agent.tools.web_fetch import WebFetchTool
    -from infra.channels.qq_channel import _read_qq_image
     from core.net.http import (
         HttpRequester,
         RequestBudget,
    @@ -43,52 +41,6 @@ async def test_default_shared_http_resources_requires_explicit_configuration():
             await resources.aclose()
     
     
    -@pytest.mark.asyncio
    -async def test_web_fetch_tool_uses_injected_requester():
    -    async def _handler(request: httpx.Request) -> httpx.Response:
    -        assert request.headers["accept"].startswith("text/plain")
    -        return httpx.Response(
    -            200,
    -            request=request,
    -            text="hello from shared requester",
    -            headers={"content-type": "text/plain; charset=utf-8"},
    -        )
    -
    -    requester = _build_requester(_handler)
    -    try:
    -        tool = WebFetchTool(requester)
    -        payload = json.loads(
    -            await tool.execute(url="https://example.com/data.txt", format="text")
    -        )
    -        assert payload["status"] == 200
    -        assert payload["text"] == "hello from shared requester"
    -    finally:
    -        await requester.client.aclose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_image_reader_uses_injected_requester():
    -    def _handler(request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            request=request,
    -            content=b"fake-image-bytes",
    -            headers={"content-type": "image/png"},
    -        )
    -
    -    requester = _build_requester(_handler)
    -    try:
    -        content, media_type = await _read_qq_image(
    -            "https://example.com/image.png",
    -            requester,
    -            max_bytes=1024,
    -        )
    -        assert content == b"fake-image-bytes"
    -        assert media_type == "image/png"
    -    finally:
    -        await requester.client.aclose()
    -
    -
     @pytest.mark.asyncio
     async def test_embedder_uses_injected_requester():
         def _handler(request: httpx.Request) -> httpx.Response:
    diff --git a/tests/test_http_resources.py b/tests/test_http_resources.py
    deleted file mode 100644
    index 450e19819..000000000
    --- a/tests/test_http_resources.py
    +++ /dev/null
    @@ -1,107 +0,0 @@
    -import asyncio
    -
    -import httpx
    -import pytest
    -
    -from core.net.http import (
    -    HttpRequester,
    -    RequestBudget,
    -    RetryPolicy,
    -    SharedHttpResources,
    -)
    -
    -
    -@pytest.mark.asyncio
    -async def test_http_requester_retries_timeout_then_succeeds():
    -    calls = {"count": 0}
    -
    -    def _handler(request: httpx.Request) -> httpx.Response:
    -        calls["count"] += 1
    -        if calls["count"] == 1:
    -            raise httpx.ReadTimeout("timeout", request=request)
    -        return httpx.Response(200, request=request, text="ok")
    -
    -    client = httpx.AsyncClient(transport=httpx.MockTransport(_handler))
    -    requester = HttpRequester(
    -        client=client,
    -        retry_policy=RetryPolicy(max_attempts=2, base_delay_s=0.0, max_delay_s=0.0),
    -        default_timeout_s=1.0,
    -        default_budget=RequestBudget(total_timeout_s=2.0),
    -        sleep=lambda _: asyncio.sleep(0),
    -    )
    -
    -    response = await requester.get("https://example.com")
    -
    -    assert response.status_code == 200
    -    assert calls["count"] == 2
    -    await client.aclose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_http_requester_retries_retryable_status_then_succeeds():
    -    calls = {"count": 0}
    -
    -    def _handler(request: httpx.Request) -> httpx.Response:
    -        calls["count"] += 1
    -        if calls["count"] == 1:
    -            return httpx.Response(503, request=request, text="retry")
    -        return httpx.Response(200, request=request, text="ok")
    -
    -    client = httpx.AsyncClient(transport=httpx.MockTransport(_handler))
    -    requester = HttpRequester(
    -        client=client,
    -        retry_policy=RetryPolicy(max_attempts=2, base_delay_s=0.0, max_delay_s=0.0),
    -        default_timeout_s=1.0,
    -        default_budget=RequestBudget(total_timeout_s=2.0),
    -        sleep=lambda _: asyncio.sleep(0),
    -    )
    -
    -    response = await requester.get("https://example.com")
    -
    -    assert response.status_code == 200
    -    assert calls["count"] == 2
    -    await client.aclose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_shared_http_resources_aclose_is_idempotent():
    -    resources = SharedHttpResources()
    -
    -    await resources.aclose()
    -    await resources.aclose()
    -
    -    assert resources.closed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_shared_http_resources_aclose_preserves_order_and_all_errors(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    resources = SharedHttpResources()
    -    close_order: list[str] = []
    -    errors = {
    -        "feed_fetcher": RuntimeError("feed cleanup failed"),
    -        "local_service": RuntimeError("local cleanup failed"),
    -    }
    -
    -    for profile, requester in (
    -        ("external_default", resources.external_default),
    -        ("feed_fetcher", resources.feed_fetcher),
    -        ("local_service", resources.local_service),
    -    ):
    -        async def _close(*, _profile: str = profile) -> None:
    -            close_order.append(_profile)
    -            if _profile in errors:
    -                raise errors[_profile]
    -
    -        monkeypatch.setattr(requester.client, "aclose", _close)
    -
    -    with pytest.raises(ExceptionGroup) as caught:
    -        await resources.aclose()
    -
    -    assert close_order == ["local_service", "feed_fetcher", "external_default"]
    -    assert caught.value.exceptions == (
    -        errors["local_service"],
    -        errors["feed_fetcher"],
    -    )
    -    assert resources.closed is True
    diff --git a/tests/test_io_modules.py b/tests/test_io_modules.py
    deleted file mode 100644
    index 54c591172..000000000
    --- a/tests/test_io_modules.py
    +++ /dev/null
    @@ -1,1156 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import base64
    -import json
    -import os
    -import signal
    -import stat
    -import sys
    -from collections.abc import Callable
    -from pathlib import Path
    -from types import SimpleNamespace
    -from types import SimpleNamespace
    -from unittest.mock import AsyncMock, MagicMock
    -from typing import cast
    -
    -import pytest
    -from PIL import Image
    -
    -import agent.mcp.client as mcp_client_module
    -import agent.tools.filesystem as filesystem_module
    -
    -from agent.mcp.client import McpClient, McpToolExecutionError, _infer_cwd
    -from agent.tool_runtime import append_tool_result
    -from agent.tools.base import ToolResult
    -from agent.tools.filesystem import (
    -    EditFileTool,
    -    ListDirTool,
    -    ReadFileTool,
    -    WriteFileTool,
    -    _READ_MAX_BYTES,
    -    _READ_MAX_LINES,
    -    _FILE_MUTATION_LOCKS,
    -    _resolve_path,
    -    _run_with_file_mutation_lock,
    -)
    -from tests.model_plugin_fakes import bind_test_model_snapshot
    -from agent.media import MAX_IMAGE_DATA_URI_BYTES, encode_image_data_uri
    -from bus.events import OutboundMessage
    -from bus.queue import MessageBus
    -
    -
    -class _Pipe:
    -    def __init__(self, lines: list[bytes] | None = None) -> None:
    -        self._lines = list(lines or [])
    -        self.writes: list[bytes] = []
    -        self.closed = False
    -
    -    def write(self, data: bytes) -> None:
    -        self.writes.append(data)
    -
    -    async def drain(self) -> None:
    -        return None
    -
    -    def close(self) -> None:
    -        self.closed = True
    -
    -    async def readline(self) -> bytes:
    -        if self._lines:
    -            return self._lines.pop(0)
    -        return b""
    -
    -
    -class _Proc:
    -    def __init__(self, stdout_lines: list[bytes], stderr_lines: list[bytes] | None = None) -> None:
    -        self.stdin = _Pipe()
    -        self.stdout = _Pipe(stdout_lines)
    -        self.stderr = _Pipe(stderr_lines)
    -        self.returncode: int | None = None
    -        self.terminated = False
    -        self.killed = False
    -
    -    def terminate(self) -> None:
    -        self.terminated = True
    -
    -    def kill(self) -> None:
    -        self.killed = True
    -
    -    async def wait(self) -> None:
    -        self.returncode = 0
    -        return None
    -
    -
    -def _as_text(value: str | ToolResult) -> str:
    -    if isinstance(value, ToolResult):
    -        return value.text
    -    return value
    -
    -
    -@pytest.mark.asyncio
    -async def test_filesystem_tools_cover_core_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
    -    base = tmp_path / "base"
    -    base.mkdir()
    -    text_file = base / "a.txt"
    -    text_file.write_text("line1\nline2\nline3\n", encoding="utf-8")
    -
    -    assert _resolve_path("a.txt", base) == text_file.resolve()
    -    with pytest.raises(PermissionError):
    -        _resolve_path("../x", base)
    -
    -    reader = ReadFileTool(base)
    -    content = await reader.execute("a.txt", offset=1, limit=1)
    -    assert "line2" in _as_text(content)
    -    assert "第 2" in _as_text(content)
    -    assert "不存在" in _as_text(await reader.execute("missing.txt"))
    -    assert "不是文件" in _as_text(await reader.execute("."))
    -
    -    outside = tmp_path / "outside.txt"
    -    outside.write_text("outside\n", encoding="utf-8")
    -    assert "outside" in _as_text(await reader.execute(str(outside)))
    -    assert "outside" in _as_text(await reader.execute("../outside.txt"))
    -
    -    image = base / "a.png"
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(image)
    -    image_provider = SimpleNamespace(input_modalities=("text", "image"))
    -    async with bind_test_model_snapshot(image_provider):
    -        image_result = await reader.execute("a.png")
    -    assert isinstance(image_result, ToolResult)
    -    assert "已读取图片文件" in image_result.text
    -    assert image_result.content_blocks[0]["type"] == "image_url"
    -    assert image_result.content_blocks[0]["image_url"]["url"].startswith(
    -        "data:image/png;base64,"
    -    )
    -
    -    weird_image = base / "image.bin"
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(weird_image, format="PNG")
    -    async with bind_test_model_snapshot(image_provider):
    -        weird_image_result = await reader.execute("image.bin")
    -    assert isinstance(weird_image_result, ToolResult)
    -    assert weird_image_result.content_blocks[0]["image_url"]["url"].startswith(
    -        "data:image/png;base64,"
    -    )
    -
    -    async with bind_test_model_snapshot(
    -        SimpleNamespace(input_modalities=("text",))
    -    ):
    -        text_model_image_result = await reader.execute("a.png")
    -    assert isinstance(text_model_image_result, str)
    -    assert "read_image_vision" in text_model_image_result
    -
    -    fake_image = base / "fake.png"
    -    fake_image.write_text("secret text", encoding="utf-8")
    -    fake_image_result = await reader.execute("fake.png")
    -    assert isinstance(fake_image_result, str)
    -    assert "secret text" in fake_image_result
    -
    -    svg = base / "icon.svg"
    -    svg.write_text("\n", encoding="utf-8")
    -    svg_result = await reader.execute("icon.svg")
    -    assert isinstance(svg_result, str)
    -    assert "" in svg_result
    -
    -    big = base / "big.png"
    -    noisy = Image.effect_noise((5000, 1000), 100).convert("RGB")
    -    noisy.save(big, format="PNG")
    -    async with bind_test_model_snapshot(image_provider):
    -        big_result = await reader.execute("big.png")
    -    assert isinstance(big_result, ToolResult)
    -    big_url = big_result.content_blocks[0]["image_url"]["url"]
    -    assert big_url.startswith("data:image/jpeg;base64,")
    -    assert len(big_url.split(",", 1)[1]) <= MAX_IMAGE_DATA_URI_BYTES
    -
    -    oversized = base / "oversized.png"
    -    oversized.write_bytes(b"\x89PNG\r\n\x1a\n")
    -    with oversized.open("r+b") as handle:
    -        handle.truncate(20 * 1024 * 1024 + 1)
    -    oversized_result = await reader.execute("oversized.png")
    -    assert isinstance(oversized_result, str)
    -    assert "单张图片不能超过 20MB" in oversized_result
    -
    -    import struct
    -    import zlib
    -
    -    pixel_bomb = base / "pixel-bomb.png"
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(pixel_bomb)
    -    payload = bytearray(pixel_bomb.read_bytes())
    -    payload[16:20] = struct.pack(">I", 10_000)
    -    payload[20:24] = struct.pack(">I", 5_000)
    -    payload[29:33] = struct.pack(">I", zlib.crc32(payload[12:29]))
    -    pixel_bomb.write_bytes(payload)
    -    pixel_result = await reader.execute("pixel-bomb.png")
    -    assert isinstance(pixel_result, str)
    -    assert "图片像素过多" in pixel_result
    -
    -    # 验证行号前缀格式(改动九)
    -    full_content = await reader.execute("a.txt")
    -    full_content = _as_text(full_content)
    -    assert "     1\u2192line1" in full_content, "read_file 应输出 '     1→line1' 格式的行号前缀"
    -    assert "     2\u2192line2" in full_content
    -    assert "     3\u2192line3" in full_content
    -
    -    # 验证字节截断后提示语包含 limit 分页引导
    -    from agent.tools import filesystem as _fs_mod
    -    orig_max_bytes = _fs_mod._READ_MAX_BYTES
    -    _fs_mod._READ_MAX_BYTES = 25  # 强制触发普通字节截断,但不触发首行超长分支
    -    truncated = await reader.execute("a.txt")
    -    _fs_mod._READ_MAX_BYTES = orig_max_bytes
    -    truncated = _as_text(truncated)
    -    assert "limit=N" in truncated, "截断提示应引导用户用 limit=N 分页,而非 offset 续读"
    -    assert "字节数超限" in truncated
    -    assert "本次返回" in truncated
    -    assert "字节" in truncated
    -    assert "offset=0 limit=100" in truncated
    -
    -    orig_max_lines = _fs_mod._READ_MAX_LINES
    -    _fs_mod._READ_MAX_LINES = 2
    -    truncated_lines = await reader.execute("a.txt")
    -    _fs_mod._READ_MAX_LINES = orig_max_lines
    -    truncated_lines = _as_text(truncated_lines)
    -    assert "行数超限" in truncated_lines
    -    assert "本次返回" in truncated_lines
    -
    -    long_line = base / "long_line.txt"
    -    long_line.write_text("x" * (_READ_MAX_BYTES + 1), encoding="utf-8")
    -    long_line_result = await reader.execute("long_line.txt")
    -    long_line_result = _as_text(long_line_result)
    -    assert "首行超过 10KB" in long_line_result
    -
    -    boundary = base / "boundary.txt"
    -    boundary.write_text("x" * (_READ_MAX_BYTES - 1), encoding="utf-8")
    -    boundary_result = await reader.execute("boundary.txt")
    -    boundary_result = _as_text(boundary_result)
    -    assert "首行超过 10KB" not in boundary_result
    -    assert "字节数超限" in boundary_result
    -
    -    bad_utf8 = base / "bad.txt"
    -    bad_utf8.write_bytes(b"ok\xffoops\n")
    -    bad_utf8_result = await reader.execute("bad.txt")
    -    bad_utf8_result = _as_text(bad_utf8_result)
    -    assert "替代字符" in bad_utf8_result
    -    assert "oops" in bad_utf8_result
    -
    -    binary = base / "data.dat"
    -    binary.write_bytes(b"\x00\x01\x02\x03hello")
    -    binary_result = await reader.execute("data.dat")
    -    binary_result = _as_text(binary_result)
    -    assert "二进制文件" in binary_result
    -    assert "xxd" in binary_result
    -
    -    text_no_read_bytes = base / "stream.txt"
    -    text_no_read_bytes.write_text("alpha\nbeta\n", encoding="utf-8")
    -    orig_read_bytes = Path.read_bytes
    -
    -    def _guard_read_bytes(self: Path):
    -        if self == text_no_read_bytes:
    -            raise AssertionError("text path should stream via open(), not Path.read_bytes()")
    -        return orig_read_bytes(self)
    -
    -    monkeypatch.setattr(Path, "read_bytes", _guard_read_bytes)
    -    streamed = await reader.execute("stream.txt")
    -    assert "alpha" in _as_text(streamed)
    -    monkeypatch.setattr(Path, "read_bytes", orig_read_bytes)
    -
    -    writer = WriteFileTool(base)
    -    result = await writer.execute("b.txt", "hello")
    -    assert "已写入" in result
    -    b_file = base / "b.txt"
    -    b_file.chmod(0o751)
    -    result = await writer.execute("b.txt", "\ufeffhello\r\n")
    -    assert "已写入" in result
    -    assert b_file.read_bytes() == "\ufeffhello\r\n".encode("utf-8")
    -    assert stat.S_IMODE(b_file.stat().st_mode) == 0o751
    -
    -    editor = EditFileTool(base)
    -    assert "未找到 old_text" in await editor.execute("b.txt", "x", "y")
    -    assert "不是文件" in await editor.execute(".", "x", "y")
    -    result = await editor.execute("b.txt", "hello", "world")
    -    assert "已成功编辑" in result
    -    assert "替换 1 处" in result, "edit_file 应在结果中报告替换数量"
    -    assert "```diff" in result
    -    assert "--- b.txt (before)" in result
    -    assert "+++ b.txt (after)" in result
    -    assert "-hello" in result
    -    assert "+world" in result
    -    assert b_file.read_bytes() == "\ufeffworld\r\n".encode("utf-8")
    -    assert stat.S_IMODE(b_file.stat().st_mode) == 0o751
    -    assert text_file.read_text(encoding="utf-8") == "line1\nline2\nline3\n"
    -
    -    dup = base / "dup.txt"
    -    dup.write_text("x\nx\n", encoding="utf-8")
    -    assert "出现了 2 次" in await editor.execute("dup.txt", "x", "y")
    -
    -    # 验证 replace_all=True(改动十)
    -    dup.write_text("x\nx\n", encoding="utf-8")
    -    result_all = await editor.execute("dup.txt", "x", "z", replace_all=True)
    -    assert "替换 2 处" in result_all, "replace_all=true 应替换所有匹配并报告数量"
    -    assert dup.read_text(encoding="utf-8") == "z\nz\n"
    -
    -    crlf = base / "crlf.txt"
    -    crlf.write_bytes(b"hello\r\nworld\r\n")
    -    result_crlf = await editor.execute("crlf.txt", "hello\nworld\n", "hi\nworld\n")
    -    assert "已成功编辑" in result_crlf
    -    assert "-hello" in result_crlf
    -    assert "+hi" in result_crlf
    -    assert crlf.read_bytes() == b"hi\r\nworld\r\n"
    -
    -    bom = base / "bom.txt"
    -    bom.write_bytes("\ufeffhello\r\n".encode("utf-8"))
    -    result_bom = await editor.execute("bom.txt", "hello\n", "world\n")
    -    assert "已成功编辑" in result_bom
    -    assert bom.read_bytes() == "\ufeffworld\r\n".encode("utf-8")
    -
    -    mixed = base / "mixed.txt"
    -    mixed.write_bytes(b"left\r\nright\nleft\nright\n")
    -    result_mixed = await editor.execute("mixed.txt", "left\nright\n", "x\ny\n")
    -    assert "已成功编辑" in result_mixed
    -    assert "替换 1 处" in result_mixed
    -    assert mixed.read_bytes() == b"left\r\nright\nx\ny\n"
    -
    -    lister = ListDirTool(base)
    -    assert "📄 a.txt" in await lister.execute(".")
    -    empty = base / "empty"
    -    empty.mkdir()
    -    assert "为空" in await lister.execute("empty")
    -    assert "不是目录" in await lister.execute("a.txt")
    -
    -
    -def test_vision_rejects_extension_only_image(tmp_path: Path):
    -    fake_image = tmp_path / "secret.png"
    -    fake_image.write_text("secret text", encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match="不支持的图片格式"):
    -        encode_image_data_uri(fake_image)
    -
    -
    -def test_vision_rejects_forged_magic_bytes_image(tmp_path: Path):
    -    fake_image = tmp_path / "secret.png"
    -    fake_image.write_bytes(b"\x89PNG\r\n\x1a\nsecret text")
    -
    -    with pytest.raises(ValueError, match="图片文件无法解码"):
    -        encode_image_data_uri(fake_image)
    -
    -
    -def test_vision_reencodes_image_before_sending(tmp_path: Path):
    -    from PIL import Image
    -
    -    image = tmp_path / "with_tail.png"
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(image)
    -    image.write_bytes(image.read_bytes() + b"secret text")
    -
    -    data_uri = encode_image_data_uri(image)
    -    payload = data_uri.split(",", 1)[1]
    -
    -    assert b"secret text" not in base64.b64decode(payload)
    -
    -
    -def test_vision_rejects_image_when_compression_still_exceeds_limit(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -):
    -    from PIL import Image
    -    from agent import media
    -
    -    image = tmp_path / "large.png"
    -    Image.new("RGB", (32, 32), (255, 0, 0)).save(image)
    -    monkeypatch.setattr(media, "MAX_IMAGE_DATA_URI_BYTES", 10)
    -
    -    with pytest.raises(ValueError, match="压缩后仍然过大"):
    -        encode_image_data_uri(image)
    -
    -
    -def test_append_tool_result_supports_multimodal_blocks() -> None:
    -    messages: list[dict] = []
    -    append_tool_result(
    -        messages,
    -        tool_call_id="call_1",
    -        tool_name="read_file",
    -        execution_status="success",
    -        content=ToolResult(
    -            text="[已读取图片文件 a.png,图片内容已提供给多模态模型]",
    -            content_blocks=[
    -                {
    -                    "type": "image_url",
    -                    "image_url": {"url": "data:image/png;base64,AAAA"},
    -                }
    -            ],
    -        ),
    -    )
    -    assert messages[0]["role"] == "tool"
    -    assert messages[0]["content"].startswith(
    -        ''
    -    )
    -    assert "[已读取图片文件" in messages[0]["content"]
    -    assert messages[1]["role"] == "user"
    -    assert messages[1]["content"][0]["type"] == "text"
    -    assert messages[1]["content"][1]["type"] == "image_url"
    -
    -
    -@pytest.mark.parametrize(
    -    "execution_status",
    -    ["success", "error", "denied", "blocked", "skipped"],
    -)
    -def test_append_tool_result_exposes_transport_status(
    -    execution_status: str,
    -) -> None:
    -    messages: list[dict] = []
    -
    -    append_tool_result(
    -        messages,
    -        tool_call_id="call_1",
    -        execution_status=execution_status,
    -        content="result",
    -    )
    -
    -    assert messages == [
    -        {
    -            "role": "tool",
    -            "tool_call_id": "call_1",
    -            "content": (
    -                f'\nresult'
    -            ),
    -        }
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_file_mutation_lock_serializes_same_file_and_allows_different_files(
    -    tmp_path: Path,
    -):
    -    _FILE_MUTATION_LOCKS.clear()
    -    shared = tmp_path / "shared.txt"
    -    other = tmp_path / "other.txt"
    -    order: list[str] = []
    -
    -    async def _job(name: str, path: Path, delay: float) -> None:
    -        async def _run() -> None:
    -            order.append(f"{name}:start")
    -            await asyncio.sleep(delay)
    -            order.append(f"{name}:end")
    -
    -        await _run_with_file_mutation_lock(path, _run)
    -
    -    shared_a = asyncio.create_task(_job("shared_a", shared, 0.05))
    -    shared_b = asyncio.create_task(_job("shared_b", shared, 0.0))
    -    other_task = asyncio.create_task(_job("other", other, 0.0))
    -    await asyncio.gather(shared_a, shared_b, other_task)
    -
    -    assert order.index("shared_a:end") < order.index("shared_b:start")
    -    assert order.index("other:start") < order.index("shared_a:end")
    -    assert not _FILE_MUTATION_LOCKS
    -
    -
    -@pytest.mark.asyncio
    -async def test_file_mutation_lock_releases_after_failure_and_cancellation(
    -    tmp_path: Path,
    -):
    -    _FILE_MUTATION_LOCKS.clear()
    -    path = tmp_path / "shared.txt"
    -
    -    async def fail() -> None:
    -        raise AssertionError("callback failed")
    -
    -    with pytest.raises(AssertionError, match="callback failed"):
    -        await _run_with_file_mutation_lock(path, fail)
    -    assert not _FILE_MUTATION_LOCKS
    -
    -    entered = asyncio.Event()
    -
    -    async def wait_forever() -> None:
    -        entered.set()
    -        await asyncio.Event().wait()
    -
    -    task = asyncio.create_task(_run_with_file_mutation_lock(path, wait_forever))
    -    await entered.wait()
    -    task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await task
    -    assert not _FILE_MUTATION_LOCKS
    -
    -
    -@pytest.mark.asyncio
    -async def test_file_mutation_lock_keeps_waiter_key_until_waiter_acquires(
    -    tmp_path: Path,
    -):
    -    _FILE_MUTATION_LOCKS.clear()
    -    path = tmp_path / "shared.txt"
    -    first_started = asyncio.Event()
    -    first_release = asyncio.Event()
    -    second_started = asyncio.Event()
    -    second_release = asyncio.Event()
    -    third_started = asyncio.Event()
    -
    -    async def first() -> None:
    -        first_started.set()
    -        await first_release.wait()
    -
    -    async def second() -> None:
    -        second_started.set()
    -        await second_release.wait()
    -
    -    async def third() -> None:
    -        third_started.set()
    -
    -    first_task = asyncio.create_task(_run_with_file_mutation_lock(path, first))
    -    await first_started.wait()
    -    second_task = asyncio.create_task(_run_with_file_mutation_lock(path, second))
    -    await asyncio.sleep(0)
    -    first_release.set()
    -    third_task: asyncio.Task[None] | None = None
    -    try:
    -        await first_task
    -        await second_started.wait()
    -        third_task = asyncio.create_task(_run_with_file_mutation_lock(path, third))
    -        await asyncio.sleep(0)
    -        assert not third_started.is_set()
    -    finally:
    -        second_release.set()
    -        await second_task
    -        if third_task is not None:
    -            await third_task
    -    assert not _FILE_MUTATION_LOCKS
    -
    -
    -@pytest.mark.asyncio
    -async def test_filesystem_tools_propagate_internal_errors(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    base = tmp_path / "base"
    -    base.mkdir()
    -    file_path = base / "file.txt"
    -    file_path.write_text("content\n", encoding="utf-8")
    -
    -    def fail_scan(*args: object) -> None:
    -        raise AssertionError("scan programming error")
    -
    -    monkeypatch.setattr(filesystem_module, "_scan_text_file", fail_scan)
    -    with pytest.raises(AssertionError, match="scan programming error"):
    -        await ReadFileTool(base).execute("file.txt")
    -
    -    def fail_atomic(*args: object, **kwargs: object) -> None:
    -        raise AssertionError("write programming error")
    -
    -    monkeypatch.setattr(filesystem_module, "atomic_write_text", fail_atomic)
    -    with pytest.raises(AssertionError, match="write programming error"):
    -        await WriteFileTool(base).execute("new.txt", "content")
    -    with pytest.raises(AssertionError, match="write programming error"):
    -        await EditFileTool(base).execute("file.txt", "content", "updated")
    -
    -    def fail_iterdir(*args: object, **kwargs: object) -> None:
    -        raise AssertionError("list programming error")
    -
    -    monkeypatch.setattr(Path, "iterdir", fail_iterdir)
    -    with pytest.raises(AssertionError, match="list programming error"):
    -        await ListDirTool(base).execute(".")
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_and_loop_factory_cover_core_paths(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    script = tmp_path / "server.py"
    -    script.write_text("print(1)", encoding="utf-8")
    -    assert _infer_cwd(["python", str(script)]) == str(tmp_path)
    -    assert _infer_cwd(["python", "srv.py"]) is None
    -
    -    proc = _Proc(
    -        [
    -            b'{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25"}}\n',
    -            b'{"jsonrpc":"2.0","method":"note"}\n',
    -            b'{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"tool1","description":"desc","inputSchema":{"type":"object"}}]}}\n',
    -            b'not json\n',
    -            b'{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"ok"}]}}\n',
    -        ],
    -        [b"warn\n", b""],
    -    )
    -    spawn = AsyncMock(return_value=proc)
    -    monkeypatch.setattr("agent.mcp.client.asyncio.create_subprocess_exec", spawn)
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "owned-boot")
    -    client = McpClient(
    -        "docs",
    -        ["python", str(script)],
    -        env={"X": "1", "AKASHIC_BOOT_ID": "spoofed-boot"},
    -    )
    -    infos = await client.connect()
    -    assert infos[0].name == "tool1"
    -    assert spawn.await_args.kwargs["env"]["X"] == "1"
    -    assert spawn.await_args.kwargs["env"]["AKASHIC_BOOT_ID"] == "owned-boot"
    -    initialize = json.loads(proc.stdin.writes[0])
    -    assert initialize["params"]["protocolVersion"] == "2025-11-25"
    -    assert await client.call("tool1", {"q": "x"}) == "ok"
    -    await client.disconnect()
    -    assert proc.stdin.closed is True
    -    assert proc.terminated is False
    -
    -    proc = _Proc([b""])
    -    monkeypatch.setattr("agent.mcp.client.asyncio.create_subprocess_exec", AsyncMock(return_value=proc))
    -    client = McpClient("docs", ["python", str(script)])
    -    client._process = proc
    -    with pytest.raises(ConnectionError):
    -        await client._recv(expected_id=1)
    -
    -
    -@pytest.mark.skipif(os.name == "nt", reason="Windows 使用 taskkill /T 覆盖进程树")
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("exit_after_list", [False, True])
    -async def test_mcp_client_cleans_wrapper_process_group(
    -    tmp_path: Path,
    -    exit_after_list: bool,
    -) -> None:
    -    """显式断开和 leader 意外退出都必须回收 stdio wrapper 后代。"""
    -    script = tmp_path / "mcp_wrapper.py"
    -    child_pid_file = tmp_path / "child.pid"
    -    _ = script.write_text(
    -        "import json, os, subprocess, sys, time\n"
    -        "from pathlib import Path\n"
    -        "child = subprocess.Popen([sys.executable, '-c', "
    -        "'import time; time.sleep(300)'])\n"
    -        "Path(os.environ['CHILD_PID_FILE']).write_text(str(child.pid))\n"
    -        "for raw in sys.stdin:\n"
    -        "    message = json.loads(raw)\n"
    -        "    method = message.get('method')\n"
    -        "    if method == 'initialize':\n"
    -        "        result = {'protocolVersion': '2025-11-25'}\n"
    -        "    elif method == 'tools/list':\n"
    -        "        result = {'tools': []}\n"
    -        "    else:\n"
    -        "        continue\n"
    -        "    print(json.dumps({'jsonrpc': '2.0', 'id': message['id'], "
    -        "'result': result}), flush=True)\n"
    -        "    if method == 'tools/list' and "
    -        "os.environ['EXIT_AFTER_LIST'] == '1':\n"
    -        "        time.sleep(0.2)\n"
    -        "        raise SystemExit(17)\n",
    -        encoding="utf-8",
    -    )
    -    client = McpClient(
    -        "wrapper",
    -        [sys.executable, str(script)],
    -        env={
    -            "CHILD_PID_FILE": str(child_pid_file),
    -            "EXIT_AFTER_LIST": "1" if exit_after_list else "0",
    -        },
    -    )
    -    child_pid = 0
    -    try:
    -        _ = await client.connect()
    -        child_pid = int(child_pid_file.read_text())
    -        assert _process_exists(child_pid)
    -
    -        if exit_after_list:
    -            await _wait_until(lambda: not client.connected)
    -            await _wait_until(lambda: not _process_exists(child_pid))
    -        else:
    -            await client.disconnect()
    -            await _wait_until(lambda: not _process_exists(child_pid))
    -    finally:
    -        await client.disconnect()
    -        if child_pid and _process_exists(child_pid):
    -            os.kill(child_pid, signal.SIGKILL)
    -
    -
    -def _process_exists(pid: int) -> bool:
    -    try:
    -        stat_fields = Path(f"/proc/{pid}/stat").read_text().rsplit(")", 1)[1].split()
    -        if stat_fields[0] == "Z":
    -            return False
    -    except OSError:
    -        pass
    -    try:
    -        os.kill(pid, 0)
    -    except ProcessLookupError:
    -        return False
    -    return True
    -
    -
    -async def _wait_until(
    -    predicate: Callable[[], bool],
    -    *,
    -    timeout_s: float = 5.0,
    -) -> None:
    -    deadline = asyncio.get_running_loop().time() + timeout_s
    -    while not predicate():
    -        if asyncio.get_running_loop().time() >= deadline:
    -            raise AssertionError("condition did not become true before timeout")
    -        await asyncio.sleep(0.05)
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_send_serializes_once_before_logging_and_writing(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    class TrackingPipe(_Pipe):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.events: list[tuple[str, bytes | None]] = []
    -
    -        def write(self, data: bytes) -> None:
    -            self.events.append(("write", data))
    -            super().write(data)
    -
    -        async def drain(self) -> None:
    -            self.events.append(("drain", None))
    -            await super().drain()
    -
    -    proc = _Proc([])
    -    proc.stdin = TrackingPipe()
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -    payload = {
    -        "jsonrpc": "2.0",
    -        "method": "测试",
    -        "params": {"text": "🙂中文" * 200},
    -    }
    -    original_dumps = mcp_client_module.json.dumps
    -    dump_calls: list[dict[str, object]] = []
    -
    -    def counting_dumps(value, *args, **kwargs):
    -        if value is payload:
    -            dump_calls.append(kwargs)
    -        return original_dumps(value, *args, **kwargs)
    -
    -    debug = MagicMock()
    -    monkeypatch.setattr(mcp_client_module.json, "dumps", counting_dumps)
    -    monkeypatch.setattr(mcp_client_module.logger, "debug", debug)
    -
    -    await client._send(payload)
    -
    -    serialized = original_dumps(payload, ensure_ascii=False)
    -    assert dump_calls == [{"ensure_ascii": False}]
    -    assert proc.stdin.writes == [(serialized + "\n").encode()]
    -    assert proc.stdin.events == [("write", proc.stdin.writes[0]), ("drain", None)]
    -    debug.assert_called_once_with("[mcp:%s] -> %s", "docs", serialized[:400])
    -
    -    unsupported = _Proc([])
    -    unsupported_client = McpClient("docs", ["python", "server.py"])
    -    unsupported_client._process = unsupported
    -    debug.reset_mock()
    -    with pytest.raises(TypeError):
    -        await unsupported_client._send({"unsupported": object()})
    -    assert unsupported.stdin.writes == []
    -    debug.assert_not_called()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_rejects_unsupported_protocol_version(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    proc = _Proc(
    -        [b'{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2099-01-01"}}\n']
    -    )
    -    monkeypatch.setattr(
    -        "agent.mcp.client.asyncio.create_subprocess_exec",
    -        AsyncMock(return_value=proc),
    -    )
    -    client = McpClient("future", ["python", "server.py"])
    -
    -    with pytest.raises(RuntimeError, match="不支持的协议版本"):
    -        await client.connect()
    -
    -    assert client.connected is False
    -    assert client._protocol_version is None
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("tool", "invalid_field"),
    -    [
    -        ({"name": "tool"}, "inputSchema"),
    -        ({"name": "tool", "inputSchema": {"type": "string"}}, "inputSchema"),
    -        (
    -            {"name": "tool", "inputSchema": {"type": "object"}, "description": 1},
    -            "description",
    -        ),
    -    ],
    -)
    -async def test_mcp_client_rejects_invalid_tool_schema(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tool: dict[str, object],
    -    invalid_field: str,
    -) -> None:
    -    proc = _Proc(
    -        [
    -            b'{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25"}}\n',
    -            (
    -                '{"jsonrpc":"2.0","id":2,"result":{"tools":['
    -                + json.dumps(tool)
    -                + "]}}\n"
    -            ).encode(),
    -        ]
    -    )
    -    monkeypatch.setattr(
    -        "agent.mcp.client.asyncio.create_subprocess_exec",
    -        AsyncMock(return_value=proc),
    -    )
    -    client = McpClient("broken", ["python", "server.py"])
    -
    -    with pytest.raises(RuntimeError, match=invalid_field):
    -        await client.connect()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_disconnect_escalates_after_graceful_timeout(
    -    monkeypatch: pytest.MonkeyPatch,
    -):
    -    proc = _Proc([])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -    wait_count = 0
    -
    -    async def wait_for(awaitable, *args, **kwargs):
    -        nonlocal wait_count
    -        wait_count += 1
    -        if wait_count == 1:
    -            awaitable.close()
    -            raise asyncio.TimeoutError
    -        return await awaitable
    -
    -    monkeypatch.setattr(mcp_client_module.asyncio, "wait_for", wait_for)
    -
    -    await client.disconnect()
    -
    -    assert proc.stdin.closed is True
    -    assert proc.terminated is True
    -    assert proc.killed is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_disconnect_kills_after_terminate_timeout(
    -    monkeypatch: pytest.MonkeyPatch,
    -):
    -    proc = _Proc([])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -    wait_count = 0
    -
    -    async def wait_for(awaitable, *args, **kwargs):
    -        nonlocal wait_count
    -        wait_count += 1
    -        if wait_count <= 2:
    -            awaitable.close()
    -            raise asyncio.TimeoutError
    -        return await awaitable
    -
    -    monkeypatch.setattr(mcp_client_module.asyncio, "wait_for", wait_for)
    -
    -    await client.disconnect()
    -
    -    assert proc.stdin.closed is True
    -    assert proc.terminated is True
    -    assert proc.killed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_disconnect_reports_cleanup_error() -> None:
    -    proc = _Proc([])
    -    proc.stdin.close = MagicMock(side_effect=OSError("stdin close failed"))
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    with pytest.raises(OSError, match="stdin close failed"):
    -        await client.disconnect()
    -
    -    assert proc.killed is True
    -    assert client._process is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_retains_ownership_when_process_group_cleanup_fails() -> None:
    -    proc = _Proc([])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    class FailingProcessGroup:
    -        async def terminate(self, *, timeout_s: float) -> None:
    -            raise RuntimeError(f"cleanup failed after {timeout_s}s")
    -
    -        async def kill(self, *, timeout_s: float) -> None:
    -            raise RuntimeError(f"cleanup failed after {timeout_s}s")
    -
    -    client._process_group = FailingProcessGroup()  # type: ignore[assignment]
    -
    -    with pytest.raises(RuntimeError, match="cleanup failed"):
    -        await client.disconnect()
    -
    -    assert client._process is proc
    -    assert client._process_group is not None
    -    client._process_group = None
    -    await client.disconnect()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("stage", ["initialize", "tools/list"])
    -async def test_mcp_client_rejects_json_rpc_error_and_closes(
    -    monkeypatch: pytest.MonkeyPatch,
    -    stage: str,
    -) -> None:
    -    responses = (
    -        [b'{"jsonrpc":"2.0","id":1,"error":{"code":-1,"message":"bad init"}}\n']
    -        if stage == "initialize"
    -        else [
    -            b'{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25"}}\n',
    -            b'{"jsonrpc":"2.0","id":2,"error":{"code":-1,"message":"bad list"}}\n',
    -        ]
    -    )
    -    proc = _Proc(responses)
    -    monkeypatch.setattr(
    -        "agent.mcp.client.asyncio.create_subprocess_exec",
    -        AsyncMock(return_value=proc),
    -    )
    -    client = McpClient("broken", ["python", "server.py"])
    -
    -    with pytest.raises(RuntimeError, match=stage):
    -        await client.connect()
    -
    -    assert client._process is None
    -    assert client._stderr_task is None
    -    assert proc.stdin.closed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_client_serializes_calls_on_same_server():
    -    class ConcurrentReadPipe(_Pipe):
    -        def __init__(self) -> None:
    -            super().__init__(
    -                [
    -                    b'{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"a"}]}}\n',
    -                    b'{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"b"}]}}\n',
    -                ]
    -            )
    -            self.reading = False
    -
    -        async def readline(self) -> bytes:
    -            if self.reading:
    -                raise RuntimeError("concurrent stdout read")
    -            self.reading = True
    -            try:
    -                await asyncio.sleep(0)
    -                return await super().readline()
    -            finally:
    -                self.reading = False
    -
    -    proc = _Proc([])
    -    proc.stdout = ConcurrentReadPipe()
    -    client = McpClient("fitbit", ["python", "server.py"])
    -    client._process = proc
    -
    -    results = await asyncio.gather(
    -        client.call("get_daily_summary", {}),
    -        client.call("get_sleep_context", {}),
    -    )
    -
    -    assert results == ["a", "b"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_call_raises_for_json_rpc_error_object() -> None:
    -    error = {"code": -1, "message": "bad call"}
    -    response = json.dumps({"jsonrpc": "2.0", "id": 1, "error": error}).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    with pytest.raises(McpToolExecutionError) as exc_info:
    -        await client.call("search", {})
    -
    -    message = str(exc_info.value)
    -    assert "JSON-RPC error" in message
    -    assert "docs" in message
    -    assert "tools/call:search" in message
    -    assert "bad call" in message
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("error", ["server unavailable", ["invalid", "error"]])
    -async def test_mcp_call_rejects_non_object_error(error: object) -> None:
    -    import json
    -
    -    response = json.dumps({"jsonrpc": "2.0", "id": 1, "error": error}).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    with pytest.raises(RuntimeError) as exc_info:
    -        await client.call("search", {})
    -
    -    message = str(exc_info.value)
    -    assert "docs" in message
    -    assert "tools/call:search" in message
    -    assert type(error).__name__ in message
    -    assert repr(error) in message
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("result", "invalid_path"),
    -    [
    -        ("plain text", "result"),
    -        ({}, "content"),
    -        ({"content": "plain text"}, "content"),
    -        ({"content": ["plain text"]}, "content[0]"),
    -        ({"content": [{}]}, "content[0].type"),
    -        ({"content": [{"type": "unknown"}]}, "content[0].type"),
    -        ({"content": [{"type": "audio"}]}, "content[0].type"),
    -        ({"content": [{"type": "resource_link"}]}, "content[0].type"),
    -        ({"content": [{"type": "text"}]}, "content[0].text"),
    -        ({"content": [{"type": "image", "mimeType": "image/png"}]}, "content[0].data"),
    -        ({"content": [{"type": "image", "data": "AAAA"}]}, "content[0].mimeType"),
    -        ({"content": [{"type": "resource"}]}, "content[0].resource"),
    -        (
    -            {"content": [{"type": "resource", "resource": {"text": "body"}}]},
    -            "content[0].resource.uri",
    -        ),
    -        (
    -            {"content": [{"type": "resource", "resource": {"uri": "r"}}]},
    -            "content[0].resource(需要 text 或 blob)",
    -        ),
    -        ({"content": [{"type": "text", "text": 123}]}, "content[0].text"),
    -        ({"content": [], "structuredContent": {}}, "structuredContent"),
    -        ({"content": [], "isError": "true"}, "isError"),
    -    ],
    -)
    -async def test_mcp_call_rejects_invalid_result_structure(
    -    result: object,
    -    invalid_path: str,
    -) -> None:
    -    response = json.dumps({"jsonrpc": "2.0", "id": 1, "result": result}).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    with pytest.raises(RuntimeError) as exc_info:
    -        await client.call("search", {})
    -
    -    message = str(exc_info.value)
    -    assert "docs" in message
    -    assert "tools/call:search" in message
    -    assert invalid_path in message
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_call_renders_valid_content_blocks() -> None:
    -    response = json.dumps(
    -        {
    -            "jsonrpc": "2.0",
    -            "id": 1,
    -            "result": {
    -                "content": [
    -                    {"type": "text", "text": "ok"},
    -                    {"type": "image", "data": "AAAA", "mimeType": "image/png"},
    -                    {
    -                        "type": "resource",
    -                        "resource": {
    -                            "uri": "resource://report",
    -                            "mimeType": "text/plain",
    -                            "text": "report",
    -                        },
    -                    },
    -                ],
    -                "isError": False,
    -            },
    -        }
    -    ).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    result = await client.call("search", {})
    -
    -    assert result.startswith("ok\n")
    -    assert '"type": "image"' in result
    -    assert '"uri": "resource://report"' in result
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_call_accepts_structured_content_for_negotiated_protocol() -> None:
    -    response = json.dumps(
    -        {
    -            "jsonrpc": "2.0",
    -            "id": 1,
    -            "result": {
    -                "content": [{"type": "text", "text": "ok"}],
    -                "structuredContent": {"result": "ok"},
    -                "isError": False,
    -            },
    -        }
    -    ).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -    client._protocol_version = "2025-11-25"
    -
    -    assert await client.call("search", {}) == "ok"
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_call_rejects_non_object_structured_content() -> None:
    -    response = json.dumps(
    -        {
    -            "jsonrpc": "2.0",
    -            "id": 1,
    -            "result": {
    -                "content": [],
    -                "structuredContent": [],
    -            },
    -        }
    -    ).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -    client._protocol_version = "2025-11-25"
    -
    -    with pytest.raises(RuntimeError, match="structuredContent(需要 object)"):
    -        await client.call("search", {})
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_call_raises_for_remote_tool_error() -> None:
    -    response = json.dumps(
    -        {
    -            "jsonrpc": "2.0",
    -            "id": 1,
    -            "result": {
    -                "content": [{"type": "text", "text": "服务端失败:限流"}],
    -                "isError": True,
    -            },
    -        },
    -        ensure_ascii=False,
    -    ).encode()
    -    proc = _Proc([response + b"\n"])
    -    client = McpClient("docs", ["python", "server.py"])
    -    client._process = proc
    -
    -    with pytest.raises(McpToolExecutionError) as exc_info:
    -        await client.call("search", {})
    -
    -    message = str(exc_info.value)
    -    assert "docs" in message
    -    assert "tools/call:search" in message
    -    assert "服务端失败:限流" in message
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_recv_timeout_includes_stage_and_recent_output(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -):
    -    script = tmp_path / "server.py"
    -    script.write_text("print(1)", encoding="utf-8")
    -    proc = _Proc([])
    -    client = McpClient("docs", ["python", str(script)])
    -    client._process = proc
    -    client._recent_stdout.append('{"jsonrpc":"2.0","method":"note"}')
    -    client._recent_stderr.append("GitHub MCP Server running on stdio")
    -
    -    async def raise_timeout(awaitable, *args, **kwargs):
    -        awaitable.close()
    -        raise asyncio.TimeoutError
    -
    -    monkeypatch.setattr(mcp_client_module.asyncio, "wait_for", raise_timeout)
    -    with pytest.raises(TimeoutError) as exc:
    -        await client._recv(expected_id=1, stage="initialize", timeout=12.0)
    -    text = str(exc.value)
    -    assert "initialize" in text
    -    assert "12s" in text
    -    assert "expected_id=1" in text
    -    assert "recent_stderr=GitHub MCP Server running on stdio" in text
    diff --git a/tests/test_json_store.py b/tests/test_json_store.py
    deleted file mode 100644
    index 8ebb621ce..000000000
    --- a/tests/test_json_store.py
    +++ /dev/null
    @@ -1,171 +0,0 @@
    -from concurrent.futures import ThreadPoolExecutor
    -import json
    -from pathlib import Path
    -import stat
    -from threading import Barrier
    -
    -import pytest
    -
    -import infra.persistence.json_store as json_store
    -from infra.persistence.json_store import atomic_save_json, atomic_write_text, load_json
    -
    -
    -def test_load_json_defaults_only_for_missing_file(tmp_path) -> None:
    -    path = tmp_path / "missing.json"
    -    assert load_json(path, default={"missing": True}) == {"missing": True}
    -
    -    path.write_bytes(b"not json")
    -    with pytest.raises(RuntimeError, match=r"\[test.state\].*missing\.json"):
    -        load_json(path, default={"fallback": True}, domain="test.state")
    -
    -
    -def test_atomic_save_json_uses_isolated_temp_files(monkeypatch, tmp_path) -> None:
    -    path = tmp_path / "state.json"
    -    replace_barrier = Barrier(2)
    -    original_replace = Path.replace
    -
    -    def synchronized_replace(source: Path, target: Path) -> Path:
    -        replace_barrier.wait(timeout=5)
    -        return original_replace(source, target)
    -
    -    monkeypatch.setattr(Path, "replace", synchronized_replace)
    -    payloads = ({"writer": "a"}, {"writer": "b"})
    -    with ThreadPoolExecutor(max_workers=2) as executor:
    -        list(executor.map(lambda payload: atomic_save_json(path, payload), payloads))
    -
    -    assert json.loads(path.read_text(encoding="utf-8")) in payloads
    -    assert list(tmp_path.glob("state.json.*.tmp")) == []
    -
    -
    -def test_atomic_save_json_cleans_own_temp_after_replace_failure(
    -    monkeypatch, tmp_path
    -) -> None:
    -    path = tmp_path / "state.json"
    -    path.write_text('{"version": "old"}', encoding="utf-8")
    -
    -    def fail_replace(source: Path, target: Path) -> Path:
    -        raise OSError("replace failed")
    -
    -    monkeypatch.setattr(Path, "replace", fail_replace)
    -    with pytest.raises(OSError, match="replace failed"):
    -        atomic_save_json(path, {"version": "new"})
    -
    -    assert json.loads(path.read_text(encoding="utf-8")) == {"version": "old"}
    -    assert list(tmp_path.glob("state.json.*.tmp")) == []
    -
    -
    -def test_atomic_save_json_cleans_temp_after_serialization_failure(
    -    monkeypatch, tmp_path
    -) -> None:
    -    path = tmp_path / "state.json"
    -    path.write_text('{"version": "old"}', encoding="utf-8")
    -
    -    def fail_after_partial_write(data, stream, **kwargs) -> None:
    -        stream.write('{"partial":')
    -        raise TypeError("serialization failed")
    -
    -    monkeypatch.setattr(json_store.json, "dump", fail_after_partial_write)
    -    with pytest.raises(TypeError, match="serialization failed"):
    -        atomic_save_json(path, {"version": "new"})
    -
    -    assert json.loads(path.read_text(encoding="utf-8")) == {"version": "old"}
    -    assert list(tmp_path.glob("state.json.*.tmp")) == []
    -
    -
    -def test_atomic_save_json_cleans_temp_after_file_fsync_failure(
    -    monkeypatch, tmp_path
    -) -> None:
    -    path = tmp_path / "state.json"
    -    path.write_text('{"version": "old"}', encoding="utf-8")
    -    calls = 0
    -
    -    def fail_file_fsync(fd: int) -> None:
    -        nonlocal calls
    -        calls += 1
    -        raise OSError("file fsync failed")
    -
    -    monkeypatch.setattr(json_store.os, "fsync", fail_file_fsync)
    -    with pytest.raises(OSError, match="file fsync failed"):
    -        atomic_save_json(path, {"version": "new"})
    -
    -    assert calls == 1
    -    assert json.loads(path.read_text(encoding="utf-8")) == {"version": "old"}
    -    assert list(tmp_path.glob("state.json.*.tmp")) == []
    -
    -
    -def test_atomic_save_json_directory_fsync_failure_keeps_new_target_visible(
    -    monkeypatch, tmp_path
    -) -> None:
    -    path = tmp_path / "state.json"
    -    path.write_text('{"version": "old"}', encoding="utf-8")
    -    calls = 0
    -    original_fsync = json_store.os.fsync
    -
    -    def fail_directory_fsync(fd: int) -> None:
    -        nonlocal calls
    -        calls += 1
    -        if calls == 2:
    -            raise OSError("directory fsync failed")
    -        original_fsync(fd)
    -
    -    monkeypatch.setattr(json_store.os, "fsync", fail_directory_fsync)
    -    with pytest.raises(OSError, match="directory fsync failed"):
    -        atomic_save_json(path, {"version": "new"})
    -
    -    assert calls == 2
    -    assert json.loads(path.read_text(encoding="utf-8")) == {"version": "new"}
    -    assert list(tmp_path.glob("state.json.*.tmp")) == []
    -
    -
    -def test_atomic_save_json_logs_cleanup_failure_without_masking_replace_error(
    -    monkeypatch, tmp_path, caplog
    -) -> None:
    -    path = tmp_path / "state.json"
    -
    -    def fail_replace(source: Path, target: Path) -> Path:
    -        raise OSError("replace failed")
    -
    -    def fail_unlink(target: Path, *, missing_ok: bool = False) -> None:
    -        raise OSError("cleanup failed")
    -
    -    monkeypatch.setattr(Path, "replace", fail_replace)
    -    monkeypatch.setattr(Path, "unlink", fail_unlink)
    -
    -    with pytest.raises(OSError, match="replace failed"):
    -        atomic_save_json(path, {"version": "new"}, domain="test.state")
    -
    -    assert "[test.state] 原子写清理临时文件失败" in caplog.text
    -    assert "cleanup failed" in caplog.text
    -
    -
    -def test_atomic_write_text_preserves_permissions_and_new_file_umask(tmp_path) -> None:
    -    existing = tmp_path / "existing.txt"
    -    existing.write_bytes(b"old")
    -    existing.chmod(0o751)
    -    atomic_write_text(existing, "\ufeffleft\r\nright\n")
    -
    -    assert existing.read_bytes() == "\ufeffleft\r\nright\n".encode("utf-8")
    -    assert stat.S_IMODE(existing.stat().st_mode) == 0o751
    -
    -    control = tmp_path / "control.txt"
    -    control.write_text("content", encoding="utf-8")
    -    new_file = tmp_path / "new.txt"
    -    atomic_write_text(new_file, "content")
    -
    -    assert stat.S_IMODE(new_file.stat().st_mode) == stat.S_IMODE(
    -        control.stat().st_mode
    -    )
    -
    -
    -def test_atomic_write_text_encoding_failure_keeps_target_and_cleans_temp(
    -    tmp_path,
    -) -> None:
    -    path = tmp_path / "state.txt"
    -    old_content = b"old\r\ncontent\n"
    -    path.write_bytes(old_content)
    -
    -    with pytest.raises(UnicodeEncodeError):
    -        atomic_write_text(path, "new\ud800")
    -
    -    assert path.read_bytes() == old_content
    -    assert list(tmp_path.glob("state.txt.*.tmp")) == []
    diff --git a/tests/test_latency_tracker.py b/tests/test_latency_tracker.py
    deleted file mode 100644
    index 42731152f..000000000
    --- a/tests/test_latency_tracker.py
    +++ /dev/null
    @@ -1,78 +0,0 @@
    -"""Tests for LatencyTracker — adaptive P90 latency estimation."""
    -
    -import statistics
    -
    -import pytest
    -
    -from agent.scheduler import LatencyTracker
    -
    -
    -class TestLatencyTrackerDefault:
    -    def test_returns_default_when_no_samples(self):
    -        t = LatencyTracker(default=25.0)
    -        assert t.lead == 25.0
    -
    -    def test_returns_default_when_one_sample(self):
    -        t = LatencyTracker(default=25.0)
    -        t.record(5.0)
    -        assert t.lead == 25.0
    -
    -    def test_returns_default_when_two_samples(self):
    -        t = LatencyTracker(default=25.0)
    -        t.record(5.0)
    -        t.record(10.0)
    -        assert t.lead == 25.0
    -
    -    def test_custom_default(self):
    -        t = LatencyTracker(default=10.0)
    -        assert t.lead == 10.0
    -
    -
    -class TestLatencyTrackerP90:
    -    def test_p90_with_sufficient_samples(self):
    -        t = LatencyTracker(default=25.0, window=20)
    -        # 20 samples from 1 to 20 seconds
    -        for i in range(1, 21):
    -            t.record(float(i))
    -        # P90 of [1..20] ≈ 18.1 (quantiles n=10 index 8 = 90th percentile)
    -        expected = statistics.quantiles(list(range(1, 21)), n=10)[8]
    -        assert abs(t.lead - expected) < 0.01
    -
    -    def test_p90_is_not_mean(self):
    -        t = LatencyTracker(default=25.0, window=20)
    -        samples = [10.0] * 18 + [100.0, 100.0]  # mostly 10s, two spikes
    -        for s in samples:
    -            t.record(s)
    -        mean = sum(samples) / len(samples)
    -        assert t.lead > mean  # P90 should be higher than mean in this skewed set
    -
    -    def test_p90_is_not_max(self):
    -        t = LatencyTracker(default=25.0, window=20)
    -        for i in range(1, 21):
    -            t.record(float(i))
    -        assert t.lead < 20.0  # must be less than max
    -
    -    def test_window_slides_old_samples_drop(self):
    -        t = LatencyTracker(default=25.0, window=5)
    -        # Fill with high latency
    -        for _ in range(5):
    -            t.record(100.0)
    -        high_lead = t.lead
    -        # Replace with low latency
    -        for _ in range(5):
    -            t.record(1.0)
    -        low_lead = t.lead
    -        assert low_lead < high_lead
    -
    -    def test_spike_raises_then_recovers(self):
    -        # Use window=5 so behavior is easy to reason about
    -        t = LatencyTracker(default=25.0, window=5)
    -        # All spikes: window full with 60s
    -        for _ in range(5):
    -            t.record(60.0)
    -        spiked_lead = t.lead
    -        # Fully recover: replace all window slots with 10s
    -        for _ in range(5):
    -            t.record(10.0)
    -        recovered_lead = t.lead
    -        assert recovered_lead < spiked_lead
    diff --git a/tests/test_lifecycle_phase.py b/tests/test_lifecycle_phase.py
    deleted file mode 100644
    index 753be1ae6..000000000
    --- a/tests/test_lifecycle_phase.py
    +++ /dev/null
    @@ -1,305 +0,0 @@
    -from __future__ import annotations
    -
    -import inspect
    -from dataclasses import dataclass
    -from datetime import datetime
    -from unittest.mock import AsyncMock
    -
    -import pytest
    -
    -from bus.event_bus import EventBus
    -from agent.lifecycle.facade import TurnLifecycle
    -from agent.lifecycle.phase import (
    -    Phase,
    -    PhaseFrame,
    -    append_string_exports,
    -    topo_sort_modules,
    -)
    -from agent.lifecycle.types import (
    -    AfterStepCtx,
    -    BeforeTurnCtx,
    -)
    -
    -
    -@dataclass
    -class _TextFrame(PhaseFrame[str, str]):
    -    pass
    -
    -
    -class _SetupModule:
    -    produces = ("text:value",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.slots["text:value"] = f"setup_{frame.input}"
    -        return frame
    -
    -
    -class _MutateModule:
    -    requires = ("text:value",)
    -    produces = ("text:value",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.slots["text:value"] = f"{frame.slots['text:value']}_mutated"
    -        return frame
    -
    -
    -class _FinalizeModule:
    -    requires = ("text:value",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.output = f"{frame.slots['text:value']}_finalized"
    -        return frame
    -
    -
    -class _FailingModule:
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        raise RuntimeError("setup failed")
    -
    -
    -class _NoOutputModule:
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        return frame
    -
    -
    -class _NeedsMissingSlotModule:
    -    requires = ("missing:value",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.output = str(frame.slots["missing:value"])
    -        return frame
    -
    -
    -class _NeedsMissingModuleSlotModule:
    -    slot = "plugin.consumer"
    -    requires = ("plugin.provider",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.output = "disabled module ran"
    -        return frame
    -
    -
    -class _BuiltinNeedsMissingModuleSlot:
    -    slot = "before_turn.consumer"
    -    requires = ("before_turn.provider",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        return frame
    -
    -
    -class _PassThroughFinalizeModule:
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.output = frame.input
    -        return frame
    -
    -
    -class _NeedsDisabledModuleSlotModule:
    -    slot = "plugin.after_consumer"
    -    requires = ("plugin.consumer",)
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        frame.output = "dependent module ran"
    -        return frame
    -
    -
    -class _PluginProviderModule:
    -    slot = "plugin.provider"
    -
    -    async def run(self, frame: _TextFrame) -> _TextFrame:
    -        return frame
    -
    -
    -def test_string_exports_reject_invalid_value_without_partial_append() -> None:
    -    target = ["existing"]
    -
    -    with pytest.raises(
    -        TypeError,
    -        match=r"key=outbound:media:image index=1 type=NoneType",
    -    ):
    -        append_string_exports(
    -            target,
    -            {"outbound:media:image": ["/tmp/a.png", None]},
    -        )
    -
    -    assert target == ["existing"]
    -
    -
    -def test_string_exports_reject_later_key_without_partial_append() -> None:
    -    target = ["existing"]
    -
    -    with pytest.raises(TypeError, match=r"key=second type=NoneType"):
    -        append_string_exports(target, {"first": "ok", "second": None})
    -
    -    assert target == ["existing"]
    -
    -
    -def test_string_exports_reject_non_list_value() -> None:
    -    with pytest.raises(TypeError, match=r"key=prompt:extra_hint:test type=dict"):
    -        append_string_exports([], {"prompt:extra_hint:test": {"text": "hint"}})
    -
    -
    -@pytest.mark.asyncio
    -async def test_phase_modules_run_in_order():
    -    phase = Phase[str, str, _TextFrame](
    -        [_SetupModule(), _MutateModule(), _FinalizeModule()],
    -        frame_factory=_TextFrame,
    -    )
    -    result = await phase.run("hello")
    -    assert result == "setup_hello_mutated_finalized"
    -
    -
    -@pytest.mark.asyncio
    -async def test_phase_modules_can_passthrough():
    -    phase = Phase[str, str, _TextFrame](
    -        [_SetupModule(), _FinalizeModule()],
    -        frame_factory=_TextFrame,
    -    )
    -    result = await phase.run("hello")
    -    assert result == "setup_hello_finalized"
    -
    -
    -@pytest.mark.asyncio
    -async def test_phase_module_exception_propagates():
    -    phase = Phase[str, str, _TextFrame]([_FailingModule()], frame_factory=_TextFrame)
    -    with pytest.raises(RuntimeError, match="setup failed"):
    -        await phase.run("x")
    -
    -
    -@pytest.mark.asyncio
    -async def test_phase_requires_output():
    -    phase = Phase[str, str, _TextFrame]([_NoOutputModule()], frame_factory=_TextFrame)
    -    with pytest.raises(RuntimeError, match="Phase 模块链未产生 output"):
    -        await phase.run("x")
    -
    -
    -def test_phase_rejects_unclosed_slot():
    -    with pytest.raises(RuntimeError, match="Phase slot 未闭合"):
    -        Phase[str, str, _TextFrame](
    -            [_NeedsMissingSlotModule()],
    -            frame_factory=_TextFrame,
    -        )
    -
    -
    -def test_phase_rejects_missing_builtin_module_dependency():
    -    with pytest.raises(RuntimeError, match="Phase 模块依赖不存在"):
    -        Phase[str, str, _TextFrame](
    -            [_BuiltinNeedsMissingModuleSlot()],
    -            frame_factory=_TextFrame,
    -        )
    -
    -
    -def test_phase_rejects_module_dependency_after_consumer():
    -    with pytest.raises(RuntimeError, match="Phase 模块依赖未满足"):
    -        Phase[str, str, _TextFrame](
    -            [_NeedsMissingModuleSlotModule(), _PluginProviderModule()],
    -            frame_factory=_TextFrame,
    -        )
    -
    -
    -def test_phase_warns_when_module_dependency_missing(
    -    caplog: pytest.LogCaptureFixture,
    -):
    -    with caplog.at_level("WARNING", logger="agent.lifecycle.phase"):
    -        Phase[str, str, _TextFrame](
    -            [_NeedsMissingModuleSlotModule()],
    -            frame_factory=_TextFrame,
    -        )
    -    assert "Phase 模块依赖不存在" in caplog.text
    -    assert "Phase slot 未闭合" not in caplog.text
    -
    -
    -@pytest.mark.asyncio
    -async def test_phase_disables_module_with_missing_module_dependency(
    -    caplog: pytest.LogCaptureFixture,
    -):
    -    with caplog.at_level("WARNING", logger="agent.lifecycle.phase"):
    -        phase = Phase[str, str, _TextFrame](
    -            [_NeedsMissingModuleSlotModule(), _PassThroughFinalizeModule()],
    -            frame_factory=_TextFrame,
    -        )
    -    result = await phase.run("hello")
    -    assert result == "hello"
    -    assert "已禁用模块" in caplog.text
    -
    -
    -def test_topo_sort_disables_missing_module_dependency_recursively(
    -    caplog: pytest.LogCaptureFixture,
    -):
    -    with caplog.at_level("WARNING", logger="agent.lifecycle.phase"):
    -        modules = topo_sort_modules(
    -            [
    -                _NeedsMissingModuleSlotModule(),
    -                _NeedsDisabledModuleSlotModule(),
    -            ]
    -        )
    -    assert modules == []
    -    assert "plugin.consumer" in caplog.text
    -    assert "plugin.after_consumer" in caplog.text
    -
    -
    -_now = datetime.now()
    -
    -
    -def test_before_turn_ctx_preserves_positional_plugin_constructor_abi() -> None:
    -    skills = ["existing-skill"]
    -    ctx = BeforeTurnCtx(
    -        "k",
    -        "c",
    -        "ch",
    -        "hello",
    -        _now,
    -        (),
    -        skills,
    -        turn_id="turn:durable",
    -    )
    -
    -    assert ctx.skill_names is skills
    -    assert ctx.turn_id == "turn:durable"
    -    assert (
    -        inspect.signature(BeforeTurnCtx).parameters["turn_id"].kind
    -        is inspect.Parameter.KEYWORD_ONLY
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_lifecycle_on_after_step():
    -    bus = EventBus()
    -    lifecycle = TurnLifecycle(bus)
    -    handler = AsyncMock(return_value=None)
    -    subscription = lifecycle.on_after_step(handler)
    -    await bus.fanout(
    -        AfterStepCtx(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=0,
    -            context_tokens_estimate=0,
    -            tools_called=(),
    -            partial_reply="",
    -            tools_used_so_far=(),
    -            tool_chain_partial=(),
    -            partial_thinking=None,
    -            has_more=True,
    -        )
    -    )
    -    handler.assert_awaited_once()
    -    assert subscription.active is True
    -
    -    subscription.close()
    -    await bus.fanout(
    -        AfterStepCtx(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=1,
    -            context_tokens_estimate=0,
    -            tools_called=(),
    -            partial_reply="",
    -            tools_used_so_far=(),
    -            tool_chain_partial=(),
    -            partial_thinking=None,
    -            has_more=False,
    -        )
    -    )
    -    assert subscription.active is False
    -    assert handler.await_count == 1
    -    assert bus.handler_count() == 0
    diff --git a/tests/test_lifecycle_phases.py b/tests/test_lifecycle_phases.py
    deleted file mode 100644
    index c5b6e1772..000000000
    --- a/tests/test_lifecycle_phases.py
    +++ /dev/null
    @@ -1,1967 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import json
    -import logging
    -import sqlite3
    -from contextlib import contextmanager
    -from datetime import datetime
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, Iterator, cast
    -from unittest.mock import AsyncMock, MagicMock, Mock
    -
    -import pytest
    -
    -from agent.context import ContextBuilder
    -from agent.plugin_composition.channels import (
    -    AttachmentKind,
    -    AttachmentRef,
    -    ChannelDeliveryReceipt,
    -    DeliveryStatus as ChannelDeliveryStatus,
    -)
    -from agent.control.context import running_turn_id
    -from agent.core.passive_support import build_context_hint_message
    -from agent.core.passive_turn import (
    -    ContextStore,
    -    PassiveTurnDeps,
    -    PassiveTurnPipeline,
    -    Reasoner,
    -)
    -from agent.core.response_parser import ResponseMetadata
    -from agent.core.runtime_support import TurnRunResult
    -from agent.control.ports import TurnUserInput
    -from agent.core.types import ContextBundle
    -from agent.lifecycle.phase import Phase
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from bus.events import (
    -    InboundMessage,
    -    OutboundMessage,
    -    TurnTerminalStatus,
    -)
    -from bus.events_lifecycle import TurnCommitted
    -from core.error_context import current_client_message_id, current_session_key
    -from infra.channels.artifacts import ChannelAttachmentArtifactStore
    -from bootstrap.channel_attachment_import import ChannelOutboundAttachmentImporter
    -from agent.lifecycle.types import (
    -    AfterReasoningCtx,
    -    AfterReasoningInput,
    -    AfterStepCtx,
    -    AfterTurnCtx,
    -    BeforeReasoningCtx,
    -    BeforeReasoningInput,
    -    BeforeStepCtx,
    -    BeforeStepInput,
    -    BeforeTurnCtx,
    -    PromptRenderCtx,
    -    PromptRenderInput,
    -    TurnSnapshot,
    -    TurnState,
    -)
    -from agent.lifecycle.phases.after_reasoning import (
    -    AfterReasoningFrame,
    -    _collect_persist_assistant_metadata,
    -    _collect_persist_user_metadata,
    -    default_after_reasoning_modules,
    -)
    -from agent.lifecycle.phases.after_step import (
    -    AfterStepFrame,
    -    default_after_step_modules,
    -)
    -from agent.lifecycle.phases.after_turn import (
    -    AfterTurnFrame,
    -    default_after_turn_modules,
    -)
    -from agent.lifecycle.phases.before_reasoning import (
    -    BeforeReasoningFrame,
    -    default_before_reasoning_modules,
    -)
    -from agent.lifecycle.phases.before_step import (
    -    BeforeStepFrame,
    -    default_before_step_modules,
    -)
    -from agent.lifecycle.phases.before_turn import (
    -    BeforeTurnFrame,
    -    default_before_turn_modules,
    -)
    -from agent.lifecycle.phases.prompt_render import (
    -    PromptRenderFrame,
    -    default_prompt_render_modules,
    -)
    -from agent.prompting import PromptSectionRender
    -from agent.persona import reset_veda
    -from agent.turns.outbound import OutboundDispatch, OutboundPort
    -from session.manager import SessionManager, logical_history_unit_ranges
    -
    -_now = datetime.now()
    -
    -
    -def open_observe_db(path: Path) -> sqlite3.Connection:
    -    path.parent.mkdir(parents=True, exist_ok=True)
    -    conn = sqlite3.connect(path)
    -    conn.row_factory = sqlite3.Row
    -    conn.execute("""
    -        CREATE TABLE IF NOT EXISTS turns (
    -            id INTEGER PRIMARY KEY AUTOINCREMENT,
    -            ts TEXT NOT NULL,
    -            source TEXT NOT NULL,
    -            session_key TEXT NOT NULL,
    -            user_msg TEXT,
    -            llm_output TEXT NOT NULL DEFAULT '',
    -            raw_llm_output TEXT,
    -            meme_tag TEXT,
    -            meme_media_count INTEGER,
    -            tool_calls TEXT,
    -            tool_chain_json TEXT,
    -            history_window INTEGER,
    -            history_messages INTEGER,
    -            history_chars INTEGER,
    -            history_tokens INTEGER,
    -            prompt_tokens INTEGER,
    -            next_turn_baseline_tokens INTEGER,
    -            error TEXT,
    -            react_iteration_count INTEGER,
    -            react_input_sum_tokens INTEGER,
    -            react_input_peak_tokens INTEGER,
    -            react_final_input_tokens INTEGER,
    -            react_cache_prompt_tokens INTEGER,
    -            react_cache_hit_tokens INTEGER
    -        )
    -        """)
    -    return conn
    -
    -
    -
    -
    -class _DummyOutbound:
    -    async def dispatch(self, outbound: OutboundDispatch) -> ChannelDeliveryReceipt:
    -        return ChannelDeliveryReceipt(
    -            delivery_id="test-delivery",
    -            status=ChannelDeliveryStatus.DELIVERED,
    -        )
    -
    -
    -
    -
    -def _format_memory_status_reply(
    -    messages: list[dict[str, object]], last_consolidated: int
    -) -> str:
    -    consolidated_user = _count_real_user_messages(messages[:last_consolidated])
    -    total_user = _count_real_user_messages(messages)
    -    pending_user = max(0, total_user - consolidated_user)
    -    last_user_message = _latest_real_user_content(messages[:last_consolidated])
    -
    -    lines = ["记忆整理状态:"]
    -    if last_consolidated <= 0 or not last_user_message:
    -        lines.append("当前会话还没有完成过记忆整理。")
    -    elif pending_user == 0:
    -        lines.append("当前会话已经整理到最新的用户消息。")
    -    else:
    -        lines.append(f"上次整理到 {pending_user} 条用户消息之前。")
    -    if last_user_message:
    -        lines.extend(
    -            ["", "最后已整理的用户消息:", f"“{_preview_text(last_user_message)}”"]
    -        )
    -    lines.extend(
    -        [
    -            "",
    -            f"尚未整理的用户消息数:{pending_user}",
    -            f"当前会话消息数:{len(messages)}",
    -        ]
    -    )
    -    return "\n".join(lines)
    -
    -
    -def _build_kvcache_reply(state: TurnState, db_path) -> str:
    -    if not db_path or not db_path.exists():
    -        return "暂无 KVCache 数据(observe 数据库不存在)。"
    -    conn = open_observe_db(db_path)
    -    try:
    -        rows = conn.execute(
    -            """SELECT llm_output, ts, react_cache_prompt_tokens, react_cache_hit_tokens
    -               FROM turns WHERE session_key=? AND source='agent'
    -               ORDER BY id DESC LIMIT ?""",
    -            [state.session_key, 5],
    -        ).fetchall()
    -    finally:
    -        conn.close()
    -    if not rows:
    -        return "暂无 KVCache 数据。"
    -    overall_prompt = sum(r[2] or 0 for r in rows)
    -    overall_hit = sum(r[3] or 0 for r in rows)
    -    overall_pct = (overall_hit / overall_prompt * 100) if overall_prompt > 0 else 0.0
    -    lines = [f"最近 {len(rows)} 轮 KVCache 状态(总命中率 {overall_pct:.2f}%)", ""]
    -    for llm_output, ts, prompt_tokens, hit_tokens in rows:
    -        content = str(llm_output or "").strip()
    -        preview = _preview_text(content.replace("\n", " "), limit=80)
    -        hit = hit_tokens or 0
    -        prompt = prompt_tokens or 0
    -        pct = (hit / prompt * 100) if prompt > 0 else 0.0
    -        lines.append(preview or "(无内容)")
    -        lines.append(_format_ts(str(ts)))
    -        lines.append(f"{hit:,} / {prompt:,}")
    -        lines.append(f"{pct:.2f}%")
    -        lines.append("")
    -    return "\n".join(lines).rstrip("\n")
    -
    -
    -def _count_real_user_messages(messages: list[dict[str, object]]) -> int:
    -    return sum(1 for item in messages if _is_real_user_message(item))
    -
    -
    -def _latest_real_user_content(messages: list[dict[str, object]]) -> str:
    -    for item in reversed(messages):
    -        if _is_real_user_message(item):
    -            return str(item.get("content", "")).strip()
    -    return ""
    -
    -
    -def _is_real_user_message(item: dict[str, object]) -> bool:
    -    content = str(item.get("content", "")).strip()
    -    return (
    -        item.get("role") == "user"
    -        and bool(content)
    -        and "data-system-context-frame" not in content
    -    )
    -
    -
    -def _preview_text(text: str, limit: int = 80) -> str:
    -    normalized = " ".join(text.split())
    -    if len(normalized) <= limit:
    -        return normalized
    -    return normalized[: limit - 1] + "…"
    -
    -
    -def _format_ts(ts: str) -> str:
    -    match = datetime.fromisoformat(ts.replace("Z", "+00:00"))
    -    return f"{match.month}-{match.day} {match:%H:%M}"
    -
    -
    -def _inbound() -> InboundMessage:
    -    return InboundMessage(
    -        channel="telegram",
    -        sender="user",
    -        chat_id="123",
    -        content="hello",
    -        timestamp=_now,
    -    )
    -
    -
    -class _DummySession:
    -    def __init__(self, key: str) -> None:
    -        self.key = key
    -        self.messages: list[dict[str, object]] = []
    -        self.metadata: dict[str, object] = {}
    -        self.last_consolidated = 0
    -
    -    def get_history(self, max_messages: int = 500) -> list[dict[str, object]]:
    -        return list(self.messages)
    -
    -    def history_units(self, *, after_seq: int = -1) -> tuple[SimpleNamespace, ...]:
    -        return (SimpleNamespace(messages=tuple(self.messages)),)
    -
    -    def add_message(
    -        self, role: str, content: str, media=None, **kwargs: object
    -    ) -> dict[str, object]:
    -        msg: dict[str, object] = {"role": role, "content": content}
    -        if media:
    -            msg["media"] = list(media)
    -        msg.update(kwargs)
    -        self.messages.append(msg)
    -        return msg
    -
    -
    -# ── BeforeTurn ──
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_setup_fills_turn_state():
    -    bus = EventBus()
    -    session = _DummySession("telegram:123")
    -
    -    session_mgr = SimpleNamespace(
    -        get_or_create=lambda key: session,
    -    )
    -
    -    bundle = ContextBundle(
    -        skill_mentions=["search"],
    -        history_messages=[{"role": "user", "content": "prev"}],
    -    )
    -    ctx_store = SimpleNamespace(
    -        prepare=AsyncMock(return_value=bundle),
    -    )
    -
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = _inbound()
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -
    -    ctx = await phase.run(state)
    -
    -    assert state.session is session
    -    assert ctx.skill_names == ["search"]
    -    assert ctx.channel == "telegram"
    -    assert ctx.chat_id == "123"
    -    assert ctx.history_messages == ({"role": "user", "content": "prev"},)
    -    assert ctx.abort is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_existing_admission_never_creates_deleted_session():
    -    bus = EventBus()
    -    session = _DummySession("mobile:deleted")
    -    get_existing = Mock(return_value=session)
    -    get_or_create = Mock(side_effect=AssertionError("不得重建已删除会话"))
    -    session_mgr = SimpleNamespace(
    -        get_existing=get_existing, get_or_create=get_or_create
    -    )
    -    ctx_store = SimpleNamespace(prepare=AsyncMock(return_value=ContextBundle()))
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = _inbound()
    -    msg.metadata = {"require_existing_session": True}
    -    state = TurnState(msg=msg, session_key="mobile:deleted", dispatch_outbound=True)
    -
    -    await phase.run(state)
    -
    -    get_existing.assert_called_once_with("mobile:deleted")
    -    get_or_create.assert_not_called()
    -    assert "require_existing_session" not in msg.metadata
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_uses_cli_session_override_context():
    -    bus = EventBus()
    -    session = _DummySession("telegram:7674283004")
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    ctx_store = SimpleNamespace(prepare=AsyncMock(return_value=ContextBundle()))
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = InboundMessage(
    -        channel="cli",
    -        sender="user",
    -        chat_id="cli-1",
    -        content="hello",
    -        timestamp=_now,
    -        metadata={
    -            "session_key_override": "telegram:7674283004",
    -            "context_channel": "telegram",
    -            "context_chat_id": "7674283004",
    -        },
    -    )
    -    state = TurnState(msg=msg, session_key=msg.session_key, dispatch_outbound=True)
    -
    -    ctx = await phase.run(state)
    -
    -    assert state.session is session
    -    assert ctx.session_key == "telegram:7674283004"
    -    assert ctx.channel == "telegram"
    -    assert ctx.chat_id == "7674283004"
    -
    -
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_context_prepare_counts_multi_input_turn_once():
    -    bus = EventBus()
    -    session = _DummySession("telegram:123")
    -    session.messages = [
    -        {
    -            "role": "user" if index < 29 else "assistant",
    -            "content": f"message-{index}",
    -            "control_turn_id": "turn-one",
    -        }
    -        for index in range(30)
    -    ]
    -    session.last_consolidated = 0
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    ctx_store = SimpleNamespace(
    -        prepare=AsyncMock(return_value=ContextBundle(history_messages=[]))
    -    )
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -
    -    ctx = await phase.run(
    -        TurnState(
    -            msg=_inbound(),
    -            session_key="telegram:123",
    -            dispatch_outbound=True,
    -        )
    -    )
    -
    -    assert ctx.abort is False
    -    ctx_store.prepare.assert_awaited_once()
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_preserves_generic_turn_effect_metadata():
    -    bus = EventBus()
    -    session = _DummySession("telegram:123")
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    ctx_store = SimpleNamespace(
    -        prepare=AsyncMock(return_value=ContextBundle(history_messages=[]))
    -    )
    -
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = _inbound()
    -    msg.metadata["effects"] = {"post_commit": "suppress"}
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -
    -    await phase.run(state)
    -
    -    assert msg.metadata["effects"] == {"post_commit": "suppress"}
    -
    -
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_projects_durable_execution_turn_id():
    -    bus = EventBus()
    -    session = _DummySession("programmatic:ctx")
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    ctx_store = SimpleNamespace(
    -        prepare=AsyncMock(
    -            return_value=ContextBundle(
    -                skill_mentions=[],
    -                history_messages=[],
    -            )
    -        )
    -    )
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = InboundMessage(
    -        channel="programmatic",
    -        sender="owner",
    -        chat_id="ctx",
    -        content="hello",
    -        timestamp=_now,
    -        metadata={"_control_execution_turn_id": "turn:durable"},
    -    )
    -
    -    ctx = await phase.run(
    -        TurnState(
    -            msg=msg,
    -            session_key="programmatic:ctx",
    -            dispatch_outbound=False,
    -        )
    -    )
    -
    -    assert ctx.turn_id == "turn:durable"
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_turn_chain_can_modify_skill_names():
    -    bus = EventBus()
    -    session = _DummySession("telegram:123")
    -
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    bundle = ContextBundle(skill_mentions=["search"])
    -    ctx_store = SimpleNamespace(prepare=AsyncMock(return_value=bundle))
    -
    -    async def add_skill(ctx):
    -        ctx.skill_names.append("added_skill")
    -        return ctx
    -
    -    bus.on(BeforeTurnCtx, add_skill)
    -
    -    phase = Phase(
    -        default_before_turn_modules(
    -            bus,
    -            cast(SessionManager, session_mgr),
    -            cast(ContextStore, ctx_store),
    -        ),
    -        frame_factory=BeforeTurnFrame,
    -    )
    -    msg = _inbound()
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -
    -    ctx = await phase.run(state)
    -    assert ctx.skill_names == ["search", "added_skill"]
    -
    -
    -# ── BeforeReasoning ──
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_reasoning_setup_calls_tools_set_context():
    -    bus = EventBus()
    -    tools = Mock()
    -    tools.set_context = Mock()
    -
    -    session = _DummySession("telegram:123")
    -    session.messages.append({"role": "user", "content": "prev", "id": "msg_42"})
    -    session_mgr = SimpleNamespace(
    -        get_or_create=lambda key: session,
    -        peek_next_message_id=lambda key: "telegram:123:0",
    -    )
    -
    -    context_builder = Mock()
    -    context_builder.render = Mock(return_value=None)
    -
    -    phase = Phase(
    -        default_before_reasoning_modules(
    -            bus,
    -            cast(ToolRegistry, tools),
    -            cast(SessionManager, session_mgr),
    -            cast(ContextBuilder, context_builder),
    -        ),
    -        frame_factory=BeforeReasoningFrame,
    -    )
    -    msg = _inbound()
    -
    -    before_turn = BeforeTurnCtx(
    -        session_key="telegram:123",
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        content=msg.content,
    -        timestamp=msg.timestamp,
    -        history_messages=(),
    -        skill_names=["search"],
    -    )
    -
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -    state.session = session
    -
    -    ctx = await phase.run(BeforeReasoningInput(state=state, before_turn=before_turn))
    -
    -    tools.set_context.assert_called_once()
    -    call_kwargs = tools.set_context.call_args[1]
    -    assert call_kwargs["channel"] == "telegram"
    -    assert call_kwargs["chat_id"] == "123"
    -    assert "current_user_source_ref" in call_kwargs
    -
    -    assert ctx.skill_names == ["search"]
    -    assert ctx.extra_hints == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_reasoning_requires_session():
    -    bus = EventBus()
    -    tools = Mock()
    -    session_mgr = Mock()
    -    context_builder = Mock()
    -
    -    phase = Phase(
    -        default_before_reasoning_modules(
    -            bus,
    -            cast(ToolRegistry, tools),
    -            cast(SessionManager, session_mgr),
    -            cast(ContextBuilder, context_builder),
    -        ),
    -        frame_factory=BeforeReasoningFrame,
    -    )
    -    msg = _inbound()
    -
    -    before_turn = BeforeTurnCtx(
    -        session_key="telegram:123",
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        content=msg.content,
    -        timestamp=msg.timestamp,
    -        history_messages=(),
    -    )
    -
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -    # session is None
    -
    -    with pytest.raises(
    -        RuntimeError, match="BeforeReasoning requires TurnState.session"
    -    ):
    -        await phase.run(BeforeReasoningInput(state=state, before_turn=before_turn))
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_reasoning_finalize_calls_render():
    -    bus = EventBus()
    -    tools = Mock()
    -    tools.set_context = Mock()
    -
    -    session = _DummySession("telegram:123")
    -    session.my_meta = {"a": 1}
    -    session_mgr = SimpleNamespace(get_or_create=lambda key: session)
    -    session_mgr.peek_next_message_id = Mock(return_value="telegram:123:0")
    -
    -    context_builder = Mock()
    -    context_builder.render = Mock(return_value=None)
    -
    -    phase = Phase(
    -        default_before_reasoning_modules(
    -            bus,
    -            cast(ToolRegistry, tools),
    -            cast(SessionManager, session_mgr),
    -            cast(ContextBuilder, context_builder),
    -        ),
    -        frame_factory=BeforeReasoningFrame,
    -    )
    -    msg = _inbound()
    -
    -    before_turn = BeforeTurnCtx(
    -        session_key="telegram:123",
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        content=msg.content,
    -        timestamp=msg.timestamp,
    -        history_messages=(),
    -        skill_names=["search"],
    -    )
    -
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -    state.session = session
    -
    -    ctx = await phase.run(BeforeReasoningInput(state=state, before_turn=before_turn))
    -
    -    context_builder.render.assert_called_once()
    -    call_args = context_builder.render.call_args[0][0]
    -    assert call_args.skill_names == ["search"]
    -    assert call_args.channel == msg.channel
    -    assert call_args.chat_id == msg.chat_id
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_reasoning_chain_can_add_extra_hints():
    -    bus = EventBus()
    -    tools = Mock()
    -    tools.set_context = Mock()
    -
    -    session = _DummySession("telegram:123")
    -    session_mgr = SimpleNamespace(
    -        get_or_create=lambda key: session,
    -        peek_next_message_id=lambda key: "telegram:123:0",
    -    )
    -
    -    context_builder = Mock()
    -    context_builder.render = Mock(return_value=None)
    -
    -    async def hint_handler(ctx):
    -        ctx.extra_hints.append("hint from plugin")
    -        return ctx
    -
    -    bus.on(BeforeReasoningCtx, hint_handler)
    -
    -    phase = Phase(
    -        default_before_reasoning_modules(
    -            bus,
    -            cast(ToolRegistry, tools),
    -            cast(SessionManager, session_mgr),
    -            cast(ContextBuilder, context_builder),
    -        ),
    -        frame_factory=BeforeReasoningFrame,
    -    )
    -    msg = _inbound()
    -
    -    before_turn = BeforeTurnCtx(
    -        session_key="telegram:123",
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        content=msg.content,
    -        timestamp=msg.timestamp,
    -        history_messages=(),
    -        extra_hints=["hint from before turn"],
    -    )
    -
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -    state.session = session
    -
    -    ctx = await phase.run(BeforeReasoningInput(state=state, before_turn=before_turn))
    -    assert ctx.extra_hints == ["hint from before turn", "hint from plugin"]
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_reasoning_chain_modify_skill_names_used_in_finalize_render():
    -    bus = EventBus()
    -    tools = Mock()
    -    tools.set_context = Mock()
    -
    -    session = _DummySession("telegram:123")
    -    session_mgr = SimpleNamespace(
    -        get_or_create=lambda key: session,
    -        peek_next_message_id=lambda key: "telegram:123:0",
    -    )
    -
    -    context_builder = Mock()
    -    context_builder.render = Mock(return_value=None)
    -
    -    async def modify_chain(ctx: BeforeReasoningCtx) -> BeforeReasoningCtx:
    -        ctx.skill_names.append("chain_added_skill")
    -        return ctx
    -
    -    bus.on(BeforeReasoningCtx, modify_chain)
    -
    -    phase = Phase(
    -        default_before_reasoning_modules(
    -            bus,
    -            cast(ToolRegistry, tools),
    -            cast(SessionManager, session_mgr),
    -            cast(ContextBuilder, context_builder),
    -        ),
    -        frame_factory=BeforeReasoningFrame,
    -    )
    -    msg = _inbound()
    -
    -    before_turn = BeforeTurnCtx(
    -        session_key="telegram:123",
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        content=msg.content,
    -        timestamp=msg.timestamp,
    -        history_messages=(),
    -        skill_names=["base_skill"],
    -    )
    -
    -    state = TurnState(msg=msg, session_key="telegram:123", dispatch_outbound=True)
    -    state.session = session
    -
    -    _ = await phase.run(BeforeReasoningInput(state=state, before_turn=before_turn))
    -
    -    # finalize 必须用 chain 修改后的值 render
    -    call_args = context_builder.render.call_args[0][0]
    -    assert "chain_added_skill" in call_args.skill_names
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_step_setup_records_token_estimate():
    -    bus = EventBus()
    -    phase = Phase(default_before_step_modules(bus), frame_factory=BeforeStepFrame)
    -    messages = [{"role": "user", "content": "hello"}]
    -
    -    ctx = await phase.run(
    -        BeforeStepInput(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=1,
    -            messages=messages,
    -            visible_names=None,
    -        )
    -    )
    -
    -    assert ctx.input_tokens_estimate > 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_prompt_render_chain_appends_bottom_section(tmp_path):
    -    bus = EventBus()
    -    _ = reset_veda(tmp_path)
    -
    -    async def append_section(ctx: PromptRenderCtx) -> PromptRenderCtx:
    -        ctx.system_sections_bottom.append(
    -            PromptSectionRender(
    -                name="plugin_protocol",
    -                content="# Plugin Protocol\n\n稳定协议",
    -                is_static=False,
    -            )
    -        )
    -        return ctx
    -
    -    bus.on(PromptRenderCtx, append_section)
    -    memory = SimpleNamespace(
    -        read_self=lambda: "",
    -        read_profile=lambda: "",
    -        get_memory_context=lambda: "",
    -    )
    -    context = ContextBuilder(tmp_path)
    -    phase = Phase(
    -        default_prompt_render_modules(bus, context),
    -        frame_factory=PromptRenderFrame,
    -    )
    -
    -    result = await phase.run(
    -        PromptRenderInput(
    -            session_key="k",
    -            channel="cli",
    -            chat_id="ch",
    -            content="hello",
    -            multimodal=True,
    -            media=None,
    -            timestamp=_now,
    -            history=[],
    -            skill_names=None,
    -            disabled_sections=set(),
    -            turn_injection_prompt="",
    -        )
    -    )
    -
    -    assert "Plugin Protocol" in str(result.messages[0]["content"])
    -
    -
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_step_finalize_injects_extra_hints():
    -    bus = EventBus()
    -
    -    async def append_hint(ctx: BeforeStepCtx) -> BeforeStepCtx:
    -        ctx.extra_hints.append("hints from plugin")
    -        return ctx
    -
    -    bus.on(BeforeStepCtx, append_hint)
    -    phase = Phase(default_before_step_modules(bus), frame_factory=BeforeStepFrame)
    -    messages = [{"role": "user", "content": "hello"}]
    -
    -    await phase.run(
    -        BeforeStepInput(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=1,
    -            messages=messages,
    -            visible_names=None,
    -        )
    -    )
    -
    -    expected = build_context_hint_message("plugin_hints", "hints from plugin")
    -    assert messages == [{"role": "user", "content": "hello"}, expected]
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_before_step_finalize_early_stop():
    -    bus = EventBus()
    -
    -    async def stop_early(ctx: BeforeStepCtx) -> BeforeStepCtx:
    -        ctx.early_stop = True
    -        ctx.early_stop_reply = "预算不足"
    -        return ctx
    -
    -    bus.on(BeforeStepCtx, stop_early)
    -    phase = Phase(default_before_step_modules(bus), frame_factory=BeforeStepFrame)
    -    messages = [{"role": "user", "content": "hello"}]
    -
    -    ctx = await phase.run(
    -        BeforeStepInput(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=1,
    -            messages=messages,
    -            visible_names=None,
    -        )
    -    )
    -
    -    assert ctx.early_stop is True
    -    assert ctx.early_stop_reply == "预算不足"
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_step_phase_runs_observers():
    -    bus = EventBus()
    -    side_effect: list[str] = []
    -
    -    async def handler(ctx: AfterStepCtx) -> None:
    -        side_effect.append(ctx.partial_reply)
    -
    -    bus.on(AfterStepCtx, handler)
    -    phase = Phase(default_after_step_modules(bus), frame_factory=AfterStepFrame)
    -    await phase.run(
    -        AfterStepCtx(
    -            session_key="k",
    -            channel="c",
    -            chat_id="ch",
    -            iteration=0,
    -            context_tokens_estimate=0,
    -            tools_called=(),
    -            partial_reply="ok",
    -            tools_used_so_far=(),
    -            tool_chain_partial=(),
    -            partial_thinking=None,
    -            has_more=True,
    -        )
    -    )
    -
    -    assert side_effect == ["ok"]
    -
    -
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_commits_outbound_attachment_binding_atomically(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    manager = SessionManager(workspace)
    -    artifact_store = ChannelAttachmentArtifactStore(
    -        workspace=workspace,
    -        session_store=manager.control_store,
    -    )
    -    source = tmp_path / "generated.png"
    -    source.write_bytes(b"generated image bytes")
    -    session = manager.get_or_create("telegram:123")
    -    state = TurnState(
    -        msg=_inbound(),
    -        session_key=session.key,
    -        dispatch_outbound=True,
    -    )
    -    state.session = session
    -    services = SimpleNamespace(
    -        presence=None,
    -        session_manager=manager,
    -        outbound_attachment_importer=ChannelOutboundAttachmentImporter(artifact_store),
    -    )
    -    phase = Phase(
    -        default_after_reasoning_modules(EventBus(), cast(Any, services)),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    result = await phase.run(
    -        AfterReasoningInput(
    -            state=state,
    -            turn_result=TurnRunResult(
    -                reply="reply",
    -                media=[str(source)],
    -            ),
    -        )
    -    )
    -
    -    assistant = session.messages[-1]
    -    attachment_ids = assistant.get("attachment_ids")
    -    assert isinstance(attachment_ids, list) and len(attachment_ids) == 1
    -    assert manager.control_store.message_attachment_ids(
    -        cast(str, assistant["id"])
    -    ) == tuple(attachment_ids)
    -    assert result.outbound.attachment_refs[0].artifact_id == attachment_ids[0]
    -    assert result.outbound.media == []
    -    manager.close()
    -
    -
    -def _assistant_metadata_ctx() -> AfterReasoningCtx:
    -    return AfterReasoningCtx(
    -        session_key="telegram:123",
    -        channel="telegram",
    -        chat_id="123",
    -        tools_used=(),
    -        thinking=None,
    -        response_metadata=ResponseMetadata(raw_text="reply"),
    -        streamed=False,
    -        tool_chain=(),
    -        context_retry={},
    -        reply="reply",
    -    )
    -
    -
    -def test_after_reasoning_rejects_fixed_assistant_metadata_field() -> None:
    -    ctx = _assistant_metadata_ctx()
    -    ctx.persist_assistant_metadata["tools_used"] = ["spoof"]
    -
    -    with pytest.raises(ValueError, match="metadata 字段不可写: tools_used"):
    -        _ = _collect_persist_assistant_metadata(ctx)
    -
    -
    -def test_after_reasoning_rejects_core_owned_user_metadata_field() -> None:
    -    ctx = _assistant_metadata_ctx()
    -    ctx.persist_user_metadata["control_turn_id"] = "spoof"
    -
    -    with pytest.raises(ValueError, match="user plugin metadata 字段不可写"):
    -        _ = _collect_persist_user_metadata(ctx)
    -
    -
    -@pytest.mark.parametrize(
    -    "field",
    -    [
    -        "control_turn_id",
    -        "turn_terminal",
    -        "turn_input_count",
    -        "skip_post_memory",
    -        "turn_duration_ms",
    -    ],
    -)
    -def test_after_reasoning_rejects_core_owned_assistant_metadata(field: str) -> None:
    -    ctx = _assistant_metadata_ctx()
    -    ctx.persist_assistant_metadata[field] = "spoof"
    -
    -    with pytest.raises(ValueError, match=f"metadata 字段不可写: {field}"):
    -        _ = _collect_persist_assistant_metadata(ctx)
    -
    -
    -@pytest.mark.asyncio
    -async def test_session_manager_adopts_pending_rows_before_post_commit_cancel(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    manager = SessionManager(tmp_path / "workspace")
    -    session = manager.get_or_create("telegram:commit-cancel")
    -    pending: list[dict[str, object]] = [
    -        {"role": "user", "content": "hello"},
    -        {"role": "assistant", "content": "reply"},
    -    ]
    -    pending_metadata = {
    -        "sentinel": "committed",
    -        "last_turn_tool_calls_count": 1,
    -    }
    -    original_persist = manager._persist_session
    -
    -    def persist_then_cancel(*args: Any, **kwargs: Any) -> int:
    -        count = original_persist(*args, **kwargs)
    -        task = asyncio.current_task()
    -        assert task is not None
    -        _ = task.cancel()
    -        return count
    -
    -    monkeypatch.setattr(manager, "_persist_session", persist_then_cancel)
    -
    -    async def append_then_checkpoint() -> None:
    -        await manager.append_messages(
    -            session,
    -            pending,
    -            metadata=pending_metadata,
    -        )
    -        await asyncio.sleep(0)
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await asyncio.create_task(append_then_checkpoint())
    -
    -    assert session.messages == pending
    -    assert [message["id"] for message in session.messages] == [
    -        "telegram:commit-cancel:0",
    -        "telegram:commit-cancel:1",
    -    ]
    -    assert session.metadata == pending_metadata
    -    manager.close()
    -    reloaded = SessionManager(tmp_path / "workspace")
    -    persisted = reloaded.get_or_create(session.key)
    -    assert persisted.messages == session.messages
    -    assert persisted.metadata == pending_metadata
    -    reloaded.close()
    -
    -
    -@pytest.mark.parametrize("failure_type", [RuntimeError, asyncio.CancelledError])
    -@pytest.mark.asyncio
    -async def test_session_metadata_stays_unchanged_when_pending_append_fails(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    failure_type: type[BaseException],
    -) -> None:
    -    manager = SessionManager(tmp_path / "workspace")
    -    session = manager.get_or_create("telegram:metadata-rollback")
    -    session.metadata = {
    -        "sentinel": "old",
    -        "last_turn_tool_calls_count": 7,
    -        "last_turn_ts": "old-ts",
    -    }
    -    manager.save(session)
    -    original_metadata = dict(session.metadata)
    -
    -    def fail_before_commit(*args: Any, **kwargs: Any) -> int:
    -        raise failure_type("injected append failure")
    -
    -    monkeypatch.setattr(manager, "_persist_session", fail_before_commit)
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(Any, SimpleNamespace(presence=None, session_manager=manager)),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    with pytest.raises(failure_type, match="injected append failure"):
    -        _ = await phase.run(
    -            AfterReasoningInput(
    -                state=TurnState(
    -                    msg=_inbound(),
    -                    session_key=session.key,
    -                    dispatch_outbound=True,
    -                    session=session,
    -                ),
    -                turn_result=TurnRunResult(
    -                    reply="reply",
    -                    tool_chain=[{"calls": [{"name": "shell"}]}],
    -                ),
    -            )
    -        )
    -
    -    assert session.messages == []
    -    assert session.metadata == original_metadata
    -    manager.close()
    -    reloaded = SessionManager(tmp_path / "workspace")
    -    persisted = reloaded.get_or_create(session.key)
    -    assert persisted.messages == []
    -    assert persisted.metadata == original_metadata
    -    reloaded.close()
    -
    -
    -
    -
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_commits_all_same_turn_users_before_final_assistant(
    -    tmp_path: Path,
    -):
    -    """保持已送达 proactive 与随后提交的 interaction 各自成单元。"""
    -
    -    class _Source:
    -        def used_inputs(self) -> tuple[TurnUserInput, ...]:
    -            return (
    -                TurnUserInput(
    -                    "i1",
    -                    0,
    -                    "u1",
    -                    (),
    -                    {"client_message_id": "client:previous-attempt"},
    -                    _now,
    -                ),
    -                TurnUserInput(
    -                    "i2",
    -                    1,
    -                    "u2",
    -                    (),
    -                    {
    -                        "client_message_id": "client:current-attempt",
    -                        "effects": {"post_commit": "suppress"},
    -                    },
    -                    _now,
    -                ),
    -            )
    -
    -    # 1. 先提交交错送达且已经结束的 proactive 单元。
    -    manager = SessionManager(tmp_path / "workspace")
    -    session = manager.get_or_create("telegram:same-turn")
    -    proactive = session.add_message(
    -        "assistant",
    -        "proactive",
    -        proactive=True,
    -        delivery_id="delivery-1",
    -    )
    -    await manager.append_messages(session, [proactive])
    -    msg = InboundMessage(
    -        channel="telegram",
    -        sender="user",
    -        chat_id="same-turn",
    -        content="u1",
    -        metadata={
    -            "control_turn_id": "turn-1",
    -            "_control_turn_input_source": _Source(),
    -        },
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(Any, SimpleNamespace(presence=None, session_manager=manager)),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    # 2. 最终 attempt 一次性提交此前累积的全部 U 和唯一 A。
    -    result = await phase.run(
    -        AfterReasoningInput(
    -            state=state,
    -            turn_result=TurnRunResult(reply="final"),
    -        )
    -    )
    -    manager.close()
    -    reloaded = SessionManager(tmp_path / "workspace")
    -    messages = reloaded.get_or_create(session.key).messages
    -
    -    # 3. 单元切分、interaction 删除都不得吞掉 proactive。
    -    assert [(item["role"], item["content"]) for item in messages] == [
    -        ("assistant", "proactive"),
    -        ("user", "u1"),
    -        ("user", "u2"),
    -        ("assistant", "final"),
    -    ]
    -    assert logical_history_unit_ranges(messages) == [(0, 1), (1, 4)]
    -    assert [item["turn_input_ordinal"] for item in messages[1:3]] == [0, 1]
    -    assert [item["timestamp"] for item in messages[1:3]] == [
    -        _now.isoformat(),
    -        _now.isoformat(),
    -    ]
    -    assert all(item["control_turn_id"] == "turn-1" for item in messages[1:])
    -    assert messages[3]["turn_terminal"] is True
    -    assert messages[3]["turn_input_count"] == 2
    -    assert messages[2]["effects"] == {"post_commit": "suppress"}
    -    assert messages[3]["effects"] == {"post_commit": "suppress"}
    -    assert result.outbound.metadata["persisted_user_message_ids"] == [
    -        messages[1]["id"],
    -        messages[2]["id"],
    -    ]
    -    assert result.outbound.metadata["persisted_user_message_id"] == messages[2]["id"]
    -    assert result.outbound.metadata["client_message_id"] == "client:current-attempt"
    -    deletion = reloaded.control_store.delete_interaction("turn-1")
    -    assert deletion is not None
    -    assert deletion.message_ids == tuple(item["id"] for item in messages[1:])
    -    assert [
    -        item["content"]
    -        for item in reloaded.control_store.fetch_session_messages(session.key)
    -    ] == ["proactive"]
    -    reloaded.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_persists_clean_mobile_reply_projection(tmp_path: Path):
    -    manager = SessionManager(tmp_path / "workspace")
    -    session = manager.get_or_create("mobile:00000000-0000-0000-0000-000000000001")
    -    merged = "【你正在回复一条历史消息】\n被回复消息:旧回答\n\n【你当前新消息】\n继续"
    -    server_received_at = datetime.fromisoformat("2026-07-16T04:04:52+00:00")
    -    msg = InboundMessage(
    -        channel="mobile",
    -        sender="device:test",
    -        chat_id="00000000-0000-0000-0000-000000000001",
    -        content=merged,
    -        timestamp=server_received_at,
    -        metadata={
    -            "client_message_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    -            "client_created_at": "2026-07-16T04:05:06+00:00",
    -            "display_content": "继续",
    -            "reply_to_message_id": "mobile:00000000-0000-0000-0000-000000000001:0",
    -            "reply_role": "assistant",
    -            "reply_preview": "旧回答",
    -        },
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(Any, SimpleNamespace(presence=None, session_manager=manager)),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    await phase.run(
    -        AfterReasoningInput(
    -            state=state,
    -            turn_result=TurnRunResult(
    -                reply="reply",
    -                context_retry={"llm_user_content": merged},
    -            ),
    -        )
    -    )
    -    manager.close()
    -    reloaded = SessionManager(tmp_path / "workspace")
    -    user = reloaded.get_or_create(session.key).messages[0]
    -
    -    assert user["content"] == "继续"
    -    assert user["timestamp"] == server_received_at.isoformat()
    -    assert user["client_created_at"] == "2026-07-16T04:05:06+00:00"
    -    assert user["llm_user_content"] == merged
    -    assert user["reply_to_message_id"].endswith(":0")
    -    assert user["reply_role"] == "assistant"
    -    assert user["reply_preview"] == "旧回答"
    -    reloaded.close()
    -
    -
    -
    -
    -@contextmanager
    -def _turn_identity(
    -    *,
    -    session_key: str,
    -    turn_id: str,
    -    client_message_id: str,
    -) -> Iterator[None]:
    -    """对齐真实 turn 边界:session_key 来自 TurnState.session_key、
    -    turn_id 是 loop owner 建立的 running_turn_id、
    -    client_message_id 来自真实 inbound metadata。"""
    -    session_token = current_session_key.set(session_key)
    -    turn_token = running_turn_id.set(turn_id)
    -    client_token = current_client_message_id.set(client_message_id)
    -    try:
    -        yield
    -    finally:
    -        current_client_message_id.reset(client_token)
    -        running_turn_id.reset(turn_token)
    -        current_session_key.reset(session_token)
    -
    -
    -def _identity_inbound(
    -    *,
    -    client_message_id: str,
    -    control_turn_id: str,
    -) -> InboundMessage:
    -    """真实 inbound 身份:client_message_id 与 control_turn_id 都在入站 metadata
    -    (loop owner 在 turn 边界写入,control_turn_id 恒等于 running_turn_id)。"""
    -    msg = _inbound()
    -    msg.metadata["client_message_id"] = client_message_id
    -    msg.metadata["control_turn_id"] = control_turn_id
    -    return msg
    -
    -
    -def _milestone_records(
    -    caplog: pytest.LogCaptureFixture,
    -    *events: str,
    -) -> list[Any]:
    -    return [
    -        record
    -        for record in caplog.records
    -        if getattr(record, "akashic_fields", {}).get("event") in events
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_append_records_success_milestones(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    appended: list[tuple[str, list[dict[str, object]]]] = []
    -
    -    async def append_messages(
    -        current: _DummySession,
    -        messages: list[dict[str, object]],
    -        *,
    -        metadata: dict[str, Any] | None = None,
    -    ) -> None:
    -        appended.append((current.key, messages))
    -
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -    session = _DummySession("telegram:123")
    -    msg = _identity_inbound(
    -        client_message_id=client_message_id,
    -        control_turn_id=turn_id,
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(
    -                Any,
    -                SimpleNamespace(
    -                    presence=None,
    -                    session_manager=SimpleNamespace(append_messages=append_messages),
    -                ),
    -            ),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(
    -            logging.INFO, logger="agent.lifecycle.phases.after_reasoning"
    -        ):
    -            result = await phase.run(
    -                AfterReasoningInput(
    -                    state=state,
    -                    turn_result=TurnRunResult(reply="reply"),
    -                )
    -            )
    -
    -    assert [key for key, _ in appended] == [state.session_key]
    -    assert [item["role"] for item in appended[0][1]] == ["user", "assistant"]
    -    persisted_user = appended[0][1][0]
    -    assert persisted_user["client_message_id"] == client_message_id
    -    assert persisted_user["control_turn_id"] == turn_id
    -    assert result.outbound.control_turn_id == turn_id
    -    assert result.outbound.execution_attempt_id == turn_id
    -    records = _milestone_records(
    -        caplog, "after_reasoning.append.start", "after_reasoning.append.done"
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_reasoning.append.start",
    -        "after_reasoning.append.done",
    -    ]
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -    start, done = records
    -    assert start.akashic_fields["duration_ms"] is None
    -    assert start.akashic_fields["origin"] == "missing"
    -    assert done.akashic_fields["duration_ms"] is not None
    -    assert done.akashic_fields["outcome"] == "done"
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_append_records_error_milestones(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    async def append_messages(
    -        current: _DummySession,
    -        messages: list[dict[str, object]],
    -        *,
    -        metadata: dict[str, Any] | None = None,
    -    ) -> None:
    -        raise RuntimeError("append exploded")
    -
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -    session = _DummySession("telegram:123")
    -    msg = _identity_inbound(
    -        client_message_id=client_message_id,
    -        control_turn_id=turn_id,
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(
    -                Any,
    -                SimpleNamespace(
    -                    presence=None,
    -                    session_manager=SimpleNamespace(append_messages=append_messages),
    -                ),
    -            ),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(
    -            logging.INFO, logger="agent.lifecycle.phases.after_reasoning"
    -        ):
    -            with pytest.raises(RuntimeError, match="append exploded"):
    -                await phase.run(
    -                    AfterReasoningInput(
    -                        state=state,
    -                        turn_result=TurnRunResult(reply="reply"),
    -                    )
    -                )
    -
    -    records = _milestone_records(
    -        caplog, "after_reasoning.append.start", "after_reasoning.append.error"
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_reasoning.append.start",
    -        "after_reasoning.append.error",
    -    ]
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -    start, error = records
    -    assert start.akashic_fields["duration_ms"] is None
    -    assert error.akashic_fields["duration_ms"] is not None
    -    assert error.akashic_fields["outcome"] == "error"
    -    assert error.levelno == logging.ERROR
    -    assert not _milestone_records(caplog, "after_reasoning.append.done")
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_reasoning_append_records_cancelled_milestone(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    async def append_messages(
    -        current: _DummySession,
    -        messages: list[dict[str, object]],
    -        *,
    -        metadata: dict[str, Any] | None = None,
    -    ) -> None:
    -        raise asyncio.CancelledError()
    -
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -    session = _DummySession("telegram:123")
    -    msg = _identity_inbound(
    -        client_message_id=client_message_id,
    -        control_turn_id=turn_id,
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    phase = Phase(
    -        default_after_reasoning_modules(
    -            EventBus(),
    -            cast(
    -                Any,
    -                SimpleNamespace(
    -                    presence=None,
    -                    session_manager=SimpleNamespace(append_messages=append_messages),
    -                ),
    -            ),
    -        ),
    -        frame_factory=AfterReasoningFrame,
    -    )
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(
    -            logging.INFO, logger="agent.lifecycle.phases.after_reasoning"
    -        ):
    -            with pytest.raises(asyncio.CancelledError):
    -                await phase.run(
    -                    AfterReasoningInput(
    -                        state=state,
    -                        turn_result=TurnRunResult(reply="reply"),
    -                    )
    -                )
    -
    -    records = _milestone_records(
    -        caplog, "after_reasoning.append.start", "after_reasoning.append.cancelled"
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_reasoning.append.start",
    -        "after_reasoning.append.cancelled",
    -    ]
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -    start, cancelled = records
    -    assert start.akashic_fields["duration_ms"] is None
    -    assert cancelled.akashic_fields["duration_ms"] is not None
    -    assert cancelled.akashic_fields["outcome"] == "cancelled"
    -    assert cancelled.levelno == logging.WARNING
    -    assert not _milestone_records(caplog, "after_reasoning.append.done")
    -
    -
    -def _after_turn_phase(
    -    bus: EventBus,
    -    *,
    -    turn_id: str = "turn:final",
    -    client_message_id: str = "cm:01",
    -) -> tuple[Phase, TurnState, _DummySession]:
    -    session = _DummySession("telegram:123")
    -    msg = _identity_inbound(
    -        client_message_id=client_message_id,
    -        control_turn_id=turn_id,
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=False)
    -    state.session = session
    -    ctx = AfterReasoningCtx(
    -        session_key=session.key,
    -        channel=msg.channel,
    -        chat_id=msg.chat_id,
    -        tools_used=(),
    -        thinking=None,
    -        response_metadata=ResponseMetadata(raw_text="reply"),
    -        streamed=False,
    -        tool_chain=(),
    -        context_retry={},
    -        reply="reply",
    -    )
    -    context = Mock()
    -    context.render = Mock(return_value=SimpleNamespace(messages=[]))
    -    context.last_debug_breakdown = []
    -    phase = Phase(
    -        default_after_turn_modules(
    -            bus,
    -            _DummyOutbound(),
    -            cast(ContextBuilder, context),
    -        ),
    -        frame_factory=AfterTurnFrame,
    -    )
    -    return phase, state, session
    -
    -
    -async def _run_after_turn(
    -    phase: Phase,
    -    state: TurnState,
    -    *,
    -    reply_to: str | None = None,
    -    media: list[str] | None = None,
    -    session_message_id: str | None = None,
    -) -> None:
    -    session = cast(_DummySession, state.session)
    -    msg = state.msg
    -    await phase.run(
    -        TurnSnapshot(
    -            state=state,
    -            outbound=OutboundMessage(
    -                channel=msg.channel,
    -                chat_id=msg.chat_id,
    -                content="reply",
    -                reply_to=reply_to,
    -                media=list(media or []),
    -                session_message_id=session_message_id,
    -                control_turn_id=str(msg.metadata.get("control_turn_id") or ""),
    -                execution_attempt_id=running_turn_id.get() or None,
    -            ),
    -            ctx=AfterReasoningCtx(
    -                session_key=session.key,
    -                channel=msg.channel,
    -                chat_id=msg.chat_id,
    -                tools_used=(),
    -                thinking=None,
    -                response_metadata=ResponseMetadata(raw_text="reply"),
    -                streamed=False,
    -                tool_chain=(),
    -                context_retry={},
    -                reply="reply",
    -            ),
    -        )
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_turn_fanout_records_returned_milestone(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    delivered: list[TurnCommitted] = []
    -    bus = EventBus()
    -    bus.on(TurnCommitted, lambda event: delivered.append(event))
    -    phase, state, _ = _after_turn_phase(bus)
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(logging.INFO, logger="agent.lifecycle.phases.after_turn"):
    -            await _run_after_turn(phase, state)
    -
    -    assert [item.turn_id for item in delivered] == [turn_id]
    -    assert [item.client_message_id for item in delivered] == [client_message_id]
    -    records = _milestone_records(
    -        caplog,
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.returned",
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.returned",
    -    ]
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -    start, returned = records
    -    assert start.akashic_fields["duration_ms"] is None
    -    assert returned.akashic_fields["duration_ms"] is not None
    -    assert returned.akashic_fields["outcome"] == "returned"
    -
    -
    -class _ExplodingFanoutBus(EventBus):
    -    async def fanout(self, event: object) -> None:
    -        raise RuntimeError("fanout exploded")
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_turn_fanout_records_error_milestone(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    phase, state, _ = _after_turn_phase(_ExplodingFanoutBus())
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(logging.INFO, logger="agent.lifecycle.phases.after_turn"):
    -            with pytest.raises(RuntimeError, match="fanout exploded"):
    -                await _run_after_turn(phase, state)
    -
    -    records = _milestone_records(
    -        caplog,
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.error",
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.error",
    -    ]
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -    start, error = records
    -    assert start.akashic_fields["duration_ms"] is None
    -    assert error.akashic_fields["duration_ms"] is not None
    -    assert error.akashic_fields["outcome"] == "error"
    -    assert error.levelno == logging.ERROR
    -    assert not _milestone_records(caplog, "after_turn.turn_committed_fanout.returned")
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_turn_committed_event_carries_client_message_id_identity(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    delivered: list[TurnCommitted] = []
    -    bus = EventBus()
    -    bus.on(TurnCommitted, lambda event: delivered.append(event))
    -    phase, state, _ = _after_turn_phase(bus)
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(logging.INFO, logger="agent.lifecycle.phases.after_turn"):
    -            await _run_after_turn(phase, state)
    -
    -    assert delivered
    -    committed = delivered[0]
    -    assert committed.session_key == state.session_key
    -    assert committed.turn_id == turn_id
    -    assert committed.client_message_id == client_message_id
    -    records = _milestone_records(
    -        caplog,
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.returned",
    -    )
    -    assert records
    -    assert {record.akashic_fields["session_id"] for record in records} == {
    -        state.session_key
    -    }
    -    assert {record.akashic_fields["turn_id"] for record in records} == {turn_id}
    -    assert {record.akashic_fields["client_message_id"] for record in records} == {
    -        client_message_id
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_turn_fanout_returns_after_observer_error(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    async def exploding_handler(event: TurnCommitted) -> None:
    -        raise RuntimeError("observer exploded")
    -
    -    bus = EventBus()
    -    bus.on(TurnCommitted, exploding_handler)
    -    phase, state, _ = _after_turn_phase(bus)
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        with caplog.at_level(logging.INFO, logger="agent.lifecycle.phases.after_turn"):
    -            await _run_after_turn(phase, state)
    -
    -    observer_errors = [
    -        record
    -        for record in caplog.records
    -        if record.name == "bus.event_bus"
    -        and "observer error for TurnCommitted" in record.getMessage()
    -    ]
    -    assert observer_errors
    -    assert "exploding_handler" in observer_errors[0].getMessage()
    -    failure_summary = [
    -        record
    -        for record in caplog.records
    -        if record.name == "bus.event_bus"
    -        and record.getMessage().startswith("fanout completed with observer errors:")
    -    ]
    -    assert failure_summary
    -    assert "failed=1 total=1" in failure_summary[0].getMessage()
    -    records = _milestone_records(
    -        caplog,
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.returned",
    -    )
    -    assert [record.akashic_fields["event"] for record in records] == [
    -        "after_turn.turn_committed_fanout.start",
    -        "after_turn.turn_committed_fanout.returned",
    -    ]
    -    assert records[1].akashic_fields["outcome"] == "returned"
    -    assert not _milestone_records(caplog, "after_turn.turn_committed_fanout.error")
    -
    -
    -@pytest.mark.asyncio
    -async def test_after_turn_dispatch_forwards_typed_identity_to_channel_port() -> None:
    -    bus = EventBus()
    -    dispatched: list[OutboundDispatch] = []
    -
    -    class _RecordingOutbound:
    -        async def dispatch(self, outbound: OutboundDispatch) -> ChannelDeliveryReceipt:
    -            dispatched.append(outbound)
    -            return ChannelDeliveryReceipt(
    -                delivery_id="delivery-final",
    -                status=ChannelDeliveryStatus.DELIVERED,
    -            )
    -
    -    outbound_port = _RecordingOutbound()
    -    turn_id = "turn:final"
    -    client_message_id = "cm:01"
    -    session = _DummySession("telegram:123")
    -    msg = _identity_inbound(
    -        client_message_id=client_message_id,
    -        control_turn_id=turn_id,
    -    )
    -    state = TurnState(msg=msg, session_key=session.key, dispatch_outbound=True)
    -    state.session = session
    -    context = Mock()
    -    context.render = Mock(return_value=SimpleNamespace(messages=[]))
    -    context.last_debug_breakdown = []
    -    phase = Phase(
    -        default_after_turn_modules(
    -            EventBus(),
    -            outbound_port,
    -            cast(ContextBuilder, context),
    -        ),
    -        frame_factory=AfterTurnFrame,
    -    )
    -
    -    with _turn_identity(
    -        session_key=state.session_key,
    -        turn_id=turn_id,
    -        client_message_id=client_message_id,
    -    ):
    -        await _run_after_turn(
    -            phase,
    -            state,
    -            reply_to="message-1",
    -            media=["/tmp/image.png"],
    -            session_message_id="telegram:123:2",
    -        )
    -
    -    assert len(dispatched) == 1
    -    outbound_message = dispatched[0]
    -    assert outbound_message.control_turn_id == turn_id
    -    assert outbound_message.execution_attempt_id == turn_id
    -    assert outbound_message.reply_to == "message-1"
    -    assert outbound_message.session_message_id == "telegram:123:2"
    -    assert outbound_message.media == ["/tmp/image.png"]
    -    assert outbound_message.content == "reply"
    -    assert outbound_message.channel == msg.channel
    -    assert outbound_message.chat_id == msg.chat_id
    -
    -
    -def _control_outbound_pipeline(
    -    session: _DummySession,
    -    *,
    -    reasoner_error: RuntimeError | None = None,
    -) -> Any:
    -    reasoner = SimpleNamespace(
    -        run_turn=AsyncMock(
    -            side_effect=(
    -                reasoner_error if reasoner_error is not None else lambda **_: None
    -            )
    -        ),
    -    )
    -    dispatch_port = AsyncMock(return_value=True)
    -    context_store = SimpleNamespace(
    -        prepare=AsyncMock(return_value=ContextBundle()),
    -    )
    -    context = SimpleNamespace(
    -        render=MagicMock(return_value=SimpleNamespace(system_prompt="p", messages=[])),
    -    )
    -    pipeline = PassiveTurnPipeline(
    -        PassiveTurnDeps(
    -            session=cast(
    -                Any,
    -                SimpleNamespace(
    -                    session_manager=SimpleNamespace(
    -                        get_or_create=MagicMock(return_value=session),
    -                        peek_next_message_id=MagicMock(return_value="telegram:123:0"),
    -                        append_messages=AsyncMock(),
    -                    ),
    -                    presence=None,
    -                ),
    -            ),
    -            context_store=cast(ContextStore, context_store),
    -            context=cast(ContextBuilder, context),
    -            tools=cast(Any, SimpleNamespace(set_context=MagicMock())),
    -            reasoner=cast(Reasoner, reasoner),
    -            outbound_port=cast(OutboundPort, dispatch_port),
    -        )
    -    )
    -    return pipeline, dispatch_port
    -
    -
    -@pytest.mark.asyncio
    -async def test_control_outbound_forwards_current_turn_id_under_turn_context() -> None:
    -    session = _DummySession("telegram:123")
    -    pipeline, dispatch_port = _control_outbound_pipeline(
    -        session,
    -        reasoner_error=RuntimeError("budget guard"),
    -    )
    -    msg = _inbound()
    -    turn_id = "turn:control"
    -    with _turn_identity(
    -        session_key="telegram:123",
    -        turn_id=turn_id,
    -        client_message_id="cm:01",
    -    ):
    -        out = await pipeline.run(msg, "telegram:123", dispatch_outbound=True)
    -
    -    assert out.content == "处理消息时出错,请稍后再试。"
    -    assert out.control_turn_id is None
    -    dispatch_port.dispatch.assert_awaited_once()
    -    dispatched = dispatch_port.dispatch.await_args.args[0]
    -    assert isinstance(dispatched, OutboundDispatch)
    -    assert dispatched.control_turn_id == turn_id
    -    assert dispatched.execution_attempt_id == turn_id
    -    assert dispatched.terminal_status is TurnTerminalStatus.FAILED
    -
    -
    -@pytest.mark.asyncio
    -async def test_control_outbound_does_not_fabricate_turn_id_without_turn() -> None:
    -    session = _DummySession("telegram:123")
    -    pipeline, dispatch_port = _control_outbound_pipeline(
    -        session,
    -        reasoner_error=RuntimeError("budget guard"),
    -    )
    -    msg = _inbound()
    -
    -    out = await pipeline.run(msg, "telegram:123", dispatch_outbound=True)
    -
    -    assert out.content == "处理消息时出错,请稍后再试。"
    -    dispatch_port.dispatch.assert_awaited_once()
    -    dispatched = dispatch_port.dispatch.await_args.args[0]
    -    assert isinstance(dispatched, OutboundDispatch)
    -    assert dispatched.control_turn_id is None
    diff --git a/tests/test_litellm_capability_catalog.py b/tests/test_litellm_capability_catalog.py
    deleted file mode 100644
    index a5cece961..000000000
    --- a/tests/test_litellm_capability_catalog.py
    +++ /dev/null
    @@ -1,537 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import fcntl
    -import gzip
    -import json
    -import os
    -import subprocess
    -import sys
    -from pathlib import Path
    -
    -import httpx
    -import pytest
    -
    -from agent.plugin_composition import (
    -    CapabilitySources,
    -    DiscoveredModel,
    -    ModelCapabilities,
    -    ModelKind,
    -)
    -from plugins.models.litellm_catalog import LiteLlmCapabilityCatalog
    -
    -
    -def _model(
    -    name: str = "deepseek-v4-flash-vision-exp",
    -    *,
    -    source: str = "unknown",
    -) -> DiscoveredModel:
    -    return DiscoveredModel(
    -        kind=ModelKind.CHAT,
    -        model=name,
    -        capabilities=ModelCapabilities(input_modalities=("text",)),
    -        capability_sources=CapabilitySources(input_modalities=source),
    -    )
    -
    -
    -def _catalog(
    -    cache_path: Path,
    -    handler: httpx.MockTransport,
    -    *,
    -    bundled: dict[str, dict[str, object]] | None = None,
    -) -> LiteLlmCapabilityCatalog:
    -    return LiteLlmCapabilityCatalog(
    -        cache_path,
    -        writable=True,
    -        transport=handler,
    -        bundled_models=bundled or {"fallback": {"supports_vision": False}},
    -        minimum_entries=1,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_remote_catalog_recognizes_exact_new_vision_model(tmp_path: Path) -> None:
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={
    -                "deepseek-v4-flash-vision-exp": {
    -                    "max_input_tokens": 1_000_000,
    -                    "max_output_tokens": 393_216,
    -                    "supports_vision": True,
    -                }
    -            },
    -            headers={"etag": '"catalog-1"'},
    -        )
    -
    -    cache_path = tmp_path / "litellm-capabilities.json"
    -    catalog = _catalog(cache_path, httpx.MockTransport(respond))
    -    result = await catalog.enrich((_model(),), provider_id="opencode-go")
    -
    -    assert len(result) == 1
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capabilities.context_window == 1_000_000
    -    assert result[0].capabilities.max_output_tokens == 393_216
    -    assert result[0].capability_sources.context_window.startswith(
    -        "litellm-remote@sha256:"
    -    )
    -    assert result[0].capability_sources.input_modalities.startswith(
    -        "litellm-remote@sha256:"
    -    )
    -    envelope = json.loads(cache_path.read_text(encoding="utf-8"))
    -    assert envelope["schema_version"] == 1
    -    assert envelope["etag"] == '"catalog-1"'
    -    assert envelope["sha256"]
    -    assert envelope["models"]["deepseek-v4-flash-vision-exp"][
    -        "max_input_tokens"
    -    ] == 1_000_000
    -
    -
    -@pytest.mark.asyncio
    -async def test_remote_catalog_bounds_gzip_before_using_fallback(tmp_path: Path) -> None:
    -    oversized = json.dumps(
    -        {"target": {"supports_vision": False, "padding": "x" * (8 * 1024 * 1024)}}
    -    ).encode()
    -
    -    async def respond(request: httpx.Request) -> httpx.Response:
    -        assert request.headers["accept-encoding"] == "gzip, identity"
    -        return httpx.Response(
    -            200,
    -            stream=httpx.ByteStream(gzip.compress(oversized)),
    -            headers={"content-encoding": "gzip"},
    -            request=request,
    -        )
    -
    -    catalog = _catalog(
    -        tmp_path / "catalog.json",
    -        httpx.MockTransport(respond),
    -        bundled={"target": {"supports_vision": True}},
    -    )
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith("litellm-wheel@")
    -
    -
    -@pytest.mark.asyncio
    -async def test_provider_capability_fact_is_not_overwritten(tmp_path: Path) -> None:
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={"deepseek-v4-flash-vision-exp": {"supports_vision": True}},
    -        )
    -
    -    catalog = _catalog(
    -        tmp_path / "catalog.json",
    -        httpx.MockTransport(respond),
    -    )
    -    original = _model(source="provider")
    -    result = await catalog.enrich((original,), provider_id="opencode-go")
    -
    -    assert result == (original,)
    -
    -
    -@pytest.mark.asyncio
    -async def test_refresh_failure_reuses_last_valid_remote_snapshot(
    -    tmp_path: Path,
    -) -> None:
    -    async def first(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={
    -                "deepseek-v4-flash-vision-exp": {
    -                    "max_input_tokens": 1_000_000,
    -                    "max_output_tokens": 393_216,
    -                    "supports_vision": True,
    -                }
    -            },
    -            headers={"etag": '"catalog-1"'},
    -        )
    -
    -    cache_path = tmp_path / "catalog.json"
    -    await _catalog(cache_path, httpx.MockTransport(first)).enrich(
    -        (_model(),), provider_id="opencode-go"
    -    )
    -
    -    async def unavailable(request: httpx.Request) -> httpx.Response:
    -        raise httpx.ConnectError("offline", request=request)
    -
    -    recovered = await _catalog(
    -        cache_path,
    -        httpx.MockTransport(unavailable),
    -    ).enrich((_model(),), provider_id="opencode-go")
    -
    -    assert recovered[0].capabilities.input_modalities == ("text", "image")
    -    assert recovered[0].capabilities.context_window == 1_000_000
    -    assert recovered[0].capabilities.max_output_tokens == 393_216
    -    assert recovered[0].capability_sources.input_modalities.startswith(
    -        "litellm-remote@sha256:"
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_first_offline_sync_uses_bundled_facts_and_keeps_unknowns(
    -    tmp_path: Path,
    -) -> None:
    -    async def unavailable(request: httpx.Request) -> httpx.Response:
    -        raise httpx.ConnectError("offline", request=request)
    -
    -    catalog = _catalog(
    -        tmp_path / "catalog.json",
    -        httpx.MockTransport(unavailable),
    -        bundled={"known-text-model": {"supports_vision": False}},
    -    )
    -    known, unknown = await catalog.enrich(
    -        (_model("known-text-model"), _model("brand-new-model")),
    -        provider_id="opencode-go",
    -    )
    -
    -    assert known.capabilities.input_modalities == ("text",)
    -    assert known.capability_sources.input_modalities.startswith("litellm-wheel@")
    -    assert unknown.capability_sources.input_modalities == "unknown"
    -
    -
    -@pytest.mark.asyncio
    -async def test_invalid_remote_and_corrupt_cache_fall_back_to_bundled(
    -    tmp_path: Path,
    -) -> None:
    -    cache_path = tmp_path / "catalog.json"
    -    cache_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "etag": '"bad"',
    -                "sha256": "not-the-model-digest",
    -                "models": {"target": {"supports_vision": True}},
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    async def invalid_json(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(200, content=b"not-json")
    -
    -    catalog = _catalog(
    -        cache_path,
    -        httpx.MockTransport(invalid_json),
    -        bundled={"target": {"supports_vision": False}},
    -    )
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text",)
    -    assert result[0].capability_sources.input_modalities.startswith("litellm-wheel@")
    -
    -
    -@pytest.mark.asyncio
    -async def test_non_utf8_cache_does_not_block_offline_sync(tmp_path: Path) -> None:
    -    cache_path = tmp_path / "catalog.json"
    -    cache_path.write_bytes(b"\xff")
    -
    -    async def unavailable(request: httpx.Request) -> httpx.Response:
    -        raise httpx.ConnectError("offline", request=request)
    -
    -    catalog = _catalog(
    -        cache_path,
    -        httpx.MockTransport(unavailable),
    -        bundled={"target": {"supports_vision": True}},
    -    )
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith("litellm-wheel@")
    -
    -
    -@pytest.mark.asyncio
    -async def test_suspiciously_small_remote_does_not_replace_last_snapshot(
    -    tmp_path: Path,
    -) -> None:
    -    async def first(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={
    -                "target": {"supports_vision": True},
    -                "stable-entry": {"supports_vision": False},
    -            },
    -        )
    -
    -    cache_path = tmp_path / "catalog.json"
    -    first_catalog = LiteLlmCapabilityCatalog(
    -        cache_path,
    -        writable=True,
    -        transport=httpx.MockTransport(first),
    -        bundled_models={
    -            "target": {"supports_vision": False},
    -            "stable-entry": {"supports_vision": False},
    -        },
    -        minimum_entries=2,
    -    )
    -    await first_catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    async def shrunk(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(200, json={"target": {"supports_vision": False}})
    -
    -    second_catalog = LiteLlmCapabilityCatalog(
    -        cache_path,
    -        writable=True,
    -        transport=httpx.MockTransport(shrunk),
    -        bundled_models={
    -            "target": {"supports_vision": False},
    -            "stable-entry": {"supports_vision": False},
    -        },
    -        minimum_entries=2,
    -    )
    -    result = await second_catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith(
    -        "litellm-remote@sha256:"
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_catalog_fuzzy_matching_keeps_provider_inventory(
    -    tmp_path: Path,
    -) -> None:
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={
    -                "deepseek-v4-flash-vision-exp": {"supports_vision": True},
    -                "another-model": {"supports_vision": True},
    -            },
    -        )
    -
    -    catalog = _catalog(tmp_path / "catalog.json", httpx.MockTransport(respond))
    -    result = await catalog.enrich(
    -        (_model("DeepSeek-V4-Flash-Vision-exp"),),
    -        provider_id="opencode-go",
    -    )
    -
    -    assert len(result) == 1
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith(
    -        "litellm-remote@sha256:"
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_each_sync_rechecks_and_can_upgrade_existing_capabilities(
    -    tmp_path: Path,
    -) -> None:
    -    requests = 0
    -
    -    async def respond(request: httpx.Request) -> httpx.Response:
    -        nonlocal requests
    -        requests += 1
    -        return httpx.Response(
    -            200,
    -            json={
    -                "deepseek-v4-flash-vision-exp": {
    -                    "supports_vision": requests > 1,
    -                }
    -            },
    -            headers={"etag": f'"catalog-{requests}"'},
    -            request=request,
    -        )
    -
    -    catalog = _catalog(
    -        tmp_path / "catalog.json",
    -        httpx.MockTransport(respond),
    -    )
    -    first = await catalog.enrich((_model(),), provider_id="opencode-go")
    -    second = await catalog.enrich((_model(),), provider_id="opencode-go")
    -
    -    assert requests == 2
    -    assert first[0].capabilities.input_modalities == ("text",)
    -    assert second[0].capabilities.input_modalities == ("text", "image")
    -
    -
    -@pytest.mark.asyncio
    -async def test_concurrent_catalog_instances_publish_in_refresh_order(
    -    tmp_path: Path,
    -) -> None:
    -    first_started = asyncio.Event()
    -    release_first = asyncio.Event()
    -    requests = 0
    -
    -    async def respond(request: httpx.Request) -> httpx.Response:
    -        nonlocal requests
    -        requests += 1
    -        if requests == 1:
    -            first_started.set()
    -            await release_first.wait()
    -            return httpx.Response(
    -                200,
    -                json={"target": {"supports_vision": False}},
    -                headers={"etag": '"catalog-1"'},
    -                request=request,
    -            )
    -        assert request.headers["if-none-match"] == '"catalog-1"'
    -        return httpx.Response(
    -            200,
    -            json={"target": {"supports_vision": True}},
    -            headers={"etag": '"catalog-2"'},
    -            request=request,
    -        )
    -
    -    cache_path = tmp_path / "catalog.json"
    -    transport = httpx.MockTransport(respond)
    -    first_catalog = _catalog(cache_path, transport)
    -    second_catalog = _catalog(cache_path, transport)
    -    first_task = asyncio.create_task(
    -        first_catalog.enrich((_model("target"),), provider_id="opencode-go")
    -    )
    -    await first_started.wait()
    -    second_task = asyncio.create_task(
    -        second_catalog.enrich((_model("target"),), provider_id="opencode-go")
    -    )
    -    await asyncio.sleep(0)
    -    release_first.set()
    -    first, second = await asyncio.gather(first_task, second_task)
    -
    -    assert first[0].capabilities.input_modalities == ("text",)
    -    assert second[0].capabilities.input_modalities == ("text", "image")
    -    envelope = json.loads(cache_path.read_text(encoding="utf-8"))
    -    assert envelope["etag"] == '"catalog-2"'
    -    assert envelope["models"]["target"]["supports_vision"] is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_cancel_while_waiting_for_catalog_lock_returns_immediately(
    -    tmp_path: Path,
    -) -> None:
    -    cache_path = tmp_path / "catalog.json"
    -    lock_fd = os.open(f"{cache_path}.lock", os.O_RDWR | os.O_CREAT, 0o600)
    -    fcntl.flock(lock_fd, fcntl.LOCK_EX)
    -
    -    async def unavailable(request: httpx.Request) -> httpx.Response:
    -        raise httpx.ConnectError("offline", request=request)
    -
    -    task = asyncio.create_task(
    -        _catalog(cache_path, httpx.MockTransport(unavailable)).enrich(
    -            (_model(),), provider_id="opencode-go"
    -        )
    -    )
    -    await asyncio.sleep(0)
    -    task.cancel()
    -    try:
    -        with pytest.raises(asyncio.CancelledError):
    -            await asyncio.wait_for(task, timeout=0.2)
    -    finally:
    -        fcntl.flock(lock_fd, fcntl.LOCK_UN)
    -        os.close(lock_fd)
    -
    -
    -@pytest.mark.asyncio
    -async def test_catalog_lock_blocks_refresh_in_another_process(tmp_path: Path) -> None:
    -    cache_path = tmp_path / "catalog.json"
    -    child = subprocess.Popen(
    -        [
    -            sys.executable,
    -            "-c",
    -            (
    -                "import fcntl, os, sys\n"
    -                "fd = os.open(sys.argv[1], os.O_RDWR | os.O_CREAT, 0o600)\n"
    -                "fcntl.flock(fd, fcntl.LOCK_EX)\n"
    -                "print('locked', flush=True)\n"
    -                "sys.stdin.read(1)\n"
    -                "fcntl.flock(fd, fcntl.LOCK_UN)\n"
    -                "os.close(fd)\n"
    -            ),
    -            f"{cache_path}.lock",
    -        ],
    -        stdin=subprocess.PIPE,
    -        stdout=subprocess.PIPE,
    -        text=True,
    -    )
    -    assert child.stdout is not None
    -    assert await asyncio.to_thread(child.stdout.readline) == "locked\n"
    -    request_started = asyncio.Event()
    -
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        request_started.set()
    -        return httpx.Response(200, json={"target": {"supports_vision": True}})
    -
    -    task = asyncio.create_task(
    -        _catalog(cache_path, httpx.MockTransport(respond)).enrich(
    -            (_model("target"),), provider_id="opencode-go"
    -        )
    -    )
    -    try:
    -        with pytest.raises(TimeoutError):
    -            await asyncio.wait_for(
    -                asyncio.shield(request_started.wait()),
    -                timeout=0.1,
    -            )
    -        assert child.stdin is not None
    -        child.stdin.write("x")
    -        child.stdin.flush()
    -        result = await asyncio.wait_for(task, timeout=2.0)
    -        assert result[0].capabilities.input_modalities == ("text", "image")
    -    finally:
    -        if not task.done():
    -            task.cancel()
    -            await asyncio.gather(task, return_exceptions=True)
    -        if child.poll() is None:
    -            child.terminate()
    -        await asyncio.to_thread(child.wait)
    -        if child.stdin is not None:
    -            child.stdin.close()
    -        if child.stdout is not None:
    -            child.stdout.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_deep_remote_json_falls_back_instead_of_failing_sync(
    -    tmp_path: Path,
    -) -> None:
    -    deep_json = b'{"target":{"nested":' + b"[" * 10_000 + b"0" + b"]" * 10_000 + b"}}"
    -
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(200, content=deep_json)
    -
    -    catalog = _catalog(
    -        tmp_path / "catalog.json",
    -        httpx.MockTransport(respond),
    -        bundled={"target": {"supports_vision": True}},
    -    )
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith("litellm-wheel@")
    -
    -
    -@pytest.mark.asyncio
    -async def test_deep_cache_json_is_ignored_during_offline_sync(tmp_path: Path) -> None:
    -    cache_path = tmp_path / "catalog.json"
    -    cache_path.write_bytes(b"[" * 10_000 + b"0" + b"]" * 10_000)
    -
    -    async def unavailable(request: httpx.Request) -> httpx.Response:
    -        raise httpx.ConnectError("offline", request=request)
    -
    -    catalog = _catalog(
    -        cache_path,
    -        httpx.MockTransport(unavailable),
    -        bundled={"target": {"supports_vision": True}},
    -    )
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text", "image")
    -    assert result[0].capability_sources.input_modalities.startswith("litellm-wheel@")
    -
    -
    -@pytest.mark.asyncio
    -async def test_unimplemented_modalities_are_not_published(tmp_path: Path) -> None:
    -    async def respond(_request: httpx.Request) -> httpx.Response:
    -        return httpx.Response(
    -            200,
    -            json={"target": {"supported_modalities": ["text", "audio", "video"]}},
    -        )
    -
    -    catalog = _catalog(tmp_path / "catalog.json", httpx.MockTransport(respond))
    -    result = await catalog.enrich((_model("target"),), provider_id="opencode-go")
    -
    -    assert result[0].capabilities.input_modalities == ("text",)
    -    assert result[0].capability_sources.input_modalities.startswith(
    -        "litellm-remote@sha256:"
    -    )
    diff --git a/tests/test_litellm_registry_resolution.py b/tests/test_litellm_registry_resolution.py
    deleted file mode 100644
    index 1cac27f20..000000000
    --- a/tests/test_litellm_registry_resolution.py
    +++ /dev/null
    @@ -1,131 +0,0 @@
    -from __future__ import annotations
    -
    -"""Tests for the LiteLLM registry resolution: online-first, fuzzy matching,
    -and local-cache fallback for model capabilities (context window etc.).
    -
    -These tests exercise the pure matching/loading helpers without hitting the
    -network or touching the live plugin-data cache. They construct a small in-memory
    -registry/cache to verify the resolution order and fuzzy matching semantics.
    -"""
    -
    -from agent.model_runtime.catalog import litellm_registry as m
    -
    -
    -def _sample_online() -> dict[str, dict]:
    -    return {
    -        "deepseek/deepseek-v4-flash": {
    -            "max_input_tokens": 1_000_000,
    -            "max_output_tokens": 393_216,
    -            "supports_vision": False,
    -        },
    -        "deepseek-v4-flash": {
    -            "max_input_tokens": 1_000_000,
    -            "max_output_tokens": 393_216,
    -            "supports_vision": False,
    -        },
    -        "azure_ai/FW-GLM-5.2": {
    -            "max_input_tokens": 1_048_576,
    -            "max_output_tokens": 131_072,
    -            "supports_vision": False,
    -        },
    -        "azure_ai/FW-Kimi-K3": {
    -            "max_input_tokens": 1_048_576,
    -            "max_output_tokens": 131_072,
    -            "supports_vision": True,
    -        },
    -    }
    -
    -
    -def test_exact_match_online() -> None:
    -    caps = m.resolve_catalog_capabilities(
    -        "openai-compatible",
    -        "deepseek/deepseek-v4-flash",
    -        base_url="https://api.commandcode.ai/provider/v1",
    -    )
    -    # This depends on the real LiteLLM registry; if the exact model is present
    -    # the context window should be positive and the source should be litellm.
    -    if caps is not None:
    -        assert caps.context_window > 0
    -        assert caps.input_modalities_known
    -
    -
    -def test_fuzzy_entry_case_insensitive() -> None:
    -    online = _sample_online()
    -    # 大小写不敏感 + 去标点:azure_ai/FW-GLM-5.2 应匹配 zai-org/GLM-5.2 的型号部分
    -    hit = m._fuzzy_entry(
    -        online,
    -        model="zai-org/GLM-5.2",
    -        provider_id="openai-compatible",
    -    )
    -    assert hit is not None
    -    assert hit.get("max_input_tokens") == 1_048_576
    -
    -
    -def test_fuzzy_entry_short_model() -> None:
    -    online = _sample_online()
    -    # 无供应商前缀的短名:glm-5.2 应通过型号包含匹配到 azure_ai/FW-GLM-5.2
    -    hit = m._fuzzy_entry(
    -        online,
    -        model="glm-5.2",
    -        provider_id="zai-org",
    -    )
    -    assert hit is not None
    -    assert hit.get("max_input_tokens") == 1_048_576
    -
    -
    -def test_fuzzy_entry_vision_modality() -> None:
    -    online = _sample_online()
    -    hit = m._fuzzy_entry(
    -        online,
    -        model="moonshotai/Kimi-K3",
    -        provider_id="openai-compatible",
    -    )
    -    assert hit is not None
    -    assert hit.get("supports_vision") is True
    -
    -
    -def test_explicit_catalog_mapping_is_used() -> None:
    -    caps = m.resolve_catalog_capabilities(
    -        "openai-compatible",
    -        "zai-org/GLM-5.2",
    -        models=_sample_online(),
    -    )
    -
    -    assert caps is not None
    -    assert caps.context_window == 1_048_576
    -
    -
    -def test_context_window_uses_input_not_sum(monkeypatch) -> None:
    -    """Context window must equal max_input_tokens (official 'context window'),
    -    not input+output. max_output_tokens is a separate generation budget."""
    -    from agent.model_runtime.catalog.litellm_registry import (
    -        resolve_catalog_capabilities,
    -    )
    -
    -    raw = {
    -        "max_input_tokens": 1_000_000,
    -        "max_output_tokens": 393_216,
    -        "supports_vision": True,
    -    }
    -    monkeypatch.setattr(m, "_model_entry", lambda provider, model: dict(raw))
    -    caps = resolve_catalog_capabilities(
    -        "openai-compatible",
    -        "deepseek/deepseek-v4-flash-vision-exp",
    -    )
    -    assert caps is not None
    -    assert caps.context_window == 1_000_000
    -    assert caps.max_output_tokens == 393_216
    -    assert "image" in caps.input_modalities
    -
    -
    -def test_context_window_fallback_when_input_missing(monkeypatch) -> None:
    -    """If max_input_tokens is absent, fall back to input+output (legacy)."""
    -    from agent.model_runtime.catalog.litellm_registry import (
    -        resolve_catalog_capabilities,
    -    )
    -
    -    raw = {"max_input_tokens": 0, "max_output_tokens": 8192}
    -    monkeypatch.setattr(m, "_model_entry", lambda provider, model: dict(raw))
    -    caps = resolve_catalog_capabilities("openai-compatible", "legacy-model")
    -    assert caps is not None
    -    assert caps.context_window == 8192
    diff --git a/tests/test_logic_modules.py b/tests/test_logic_modules.py
    deleted file mode 100644
    index fff4975e3..000000000
    --- a/tests/test_logic_modules.py
    +++ /dev/null
    @@ -1,821 +0,0 @@
    -from __future__ import annotations
    -from typing import Any, cast
    -
    -import asyncio
    -import json
    -import logging
    -import sqlite3
    -from concurrent.futures import ThreadPoolExecutor
    -from datetime import datetime, timezone
    -from pathlib import Path
    -from types import SimpleNamespace
    -from unittest.mock import AsyncMock, MagicMock
    -
    -import pytest
    -
    -from agent.prompting import is_context_frame
    -from session.manager import (
    -    Session,
    -    SessionManager,
    -    _STORED_TOOL_RESULT_CHAR_BUDGET,
    -    _TOOL_RESULT_CHAR_BUDGET,
    -)
    -from session.store import SessionStore
    -
    -
    -@pytest.mark.parametrize(
    -    "payload",
    -    ["{broken", "[]", "", sqlite3.Binary(b"\xff"), sqlite3.Binary(b"")],
    -)
    -def test_session_metadata_corruption_fails_at_database_boundary(
    -    tmp_path: Path,
    -    payload: object,
    -) -> None:
    -    manager = SessionManager(tmp_path)
    -    session_key = "telegram:broken"
    -    manager.get_or_create(session_key)
    -    manager._store._conn.execute(
    -        "UPDATE sessions SET metadata = ? WHERE key = ?",
    -        (payload, session_key),
    -    )
    -    manager._store._conn.commit()
    -
    -    with pytest.raises(ValueError, match=session_key):
    -        manager.get_channel_metadata("telegram")
    -    with pytest.raises(ValueError, match=session_key):
    -        manager._store.get_session_meta(session_key)
    -    with pytest.raises(ValueError, match=session_key):
    -        manager._store.list_sessions_for_dashboard()
    -
    -
    -def test_session_manager_rejects_orphan_messages_without_metadata(
    -    tmp_path: Path,
    -) -> None:
    -    manager = SessionManager(tmp_path)
    -    try:
    -        manager._store.insert_message(
    -            "telegram:orphan",
    -            role="user",
    -            content="孤立消息",
    -            ts="2026-07-13T00:00:00+00:00",
    -            seq=0,
    -        )
    -
    -        with pytest.raises(ValueError, match="session metadata 缺失"):
    -            manager.get_or_create("telegram:orphan")
    -    finally:
    -        manager.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_session_batch_persistence_rolls_back_all_messages_on_failure(
    -    tmp_path: Path,
    -) -> None:
    -    manager = SessionManager(tmp_path)
    -    session = manager.get_or_create("telegram:atomic")
    -    session.add_message("user", "第一条")
    -    session.add_message("assistant", "第二条")
    -    manager._store._conn.execute("""
    -        CREATE TRIGGER reject_assistant_message
    -        BEFORE INSERT ON messages
    -        WHEN NEW.role = 'assistant'
    -        BEGIN
    -            SELECT RAISE(ABORT, '测试写入失败');
    -        END
    -        """)
    -    manager._store._conn.commit()
    -
    -    with pytest.raises(sqlite3.IntegrityError, match="测试写入失败"):
    -        await manager.append_messages(session, session.messages)
    -
    -    assert manager._store.count_messages(session.key) == 0
    -    assert all("id" not in message for message in session.messages)
    -
    -
    -@pytest.mark.asyncio
    -async def test_session_batch_persistence_uses_one_commit(tmp_path: Path) -> None:
    -    manager = SessionManager(tmp_path)
    -    session = manager.get_or_create("telegram:batch")
    -    session.add_message("user", "第一条")
    -    session.add_message("assistant", "第二条")
    -    statements: list[str] = []
    -    manager._store._conn.set_trace_callback(statements.append)
    -
    -    await manager.append_messages(session, session.messages)
    -
    -    assert statements.count("COMMIT") == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_session_persistence_truncates_each_large_tool_result(
    -    tmp_path: Path,
    -) -> None:
    -    manager = SessionManager(tmp_path)
    -    session = manager.get_or_create("telegram:bounded-tool-result")
    -    long_result = "head-" + "x" * (_STORED_TOOL_RESULT_CHAR_BUDGET + 200) + "-tail"
    -    session.add_message("assistant", "结论")
    -    session.messages[-1]["tool_chain"] = [
    -        {
    -            "text": "查询",
    -            "calls": [
    -                {
    -                    "call_id": "call-1",
    -                    "name": "shell",
    -                    "arguments": {},
    -                    "result": long_result,
    -                },
    -                {
    -                    "call_id": "call-2",
    -                    "name": "shell",
    -                    "arguments": {},
    -                    "result": "small",
    -                },
    -            ],
    -        }
    -    ]
    -
    -    await manager.append_messages(session, session.messages)
    -    manager.close()
    -    reloaded_manager = SessionManager(tmp_path)
    -    stored_session = reloaded_manager.get_existing(session.key)
    -    stored_chain = cast(
    -        list[dict[str, object]],
    -        stored_session.messages[0]["tool_chain"],
    -    )
    -    stored_calls = cast(list[dict[str, object]], stored_chain[0]["calls"])
    -    stored_result = cast(str, stored_calls[0]["result"])
    -
    -    assert len(stored_result) == _STORED_TOOL_RESULT_CHAR_BUDGET
    -    assert stored_result.startswith("head-")
    -    assert stored_result.endswith("-tail")
    -    assert "chars truncated before persistence" in stored_result
    -    assert stored_calls[1]["result"] == "small"
    -    assert long_result.endswith("-tail")
    -    reloaded_manager.close()
    -
    -
    -def test_session_manager_preserves_message_extra_payload_order(tmp_path: Path) -> None:
    -    manager = SessionManager(tmp_path)
    -    session = manager.get_or_create("telegram:extra-order")
    -    session.messages.append(
    -        {
    -            "role": "user",
    -            "custom_first": "先",
    -            "session_key": "must-skip",
    -            "content": "正文",
    -            "custom_second": {"nested": "值"},
    -            "seq": 999,
    -            "timestamp": "2026-07-23T00:00:00+00:00",
    -            "tool_chain": None,
    -        }
    -    )
    -
    -    manager.save(session)
    -
    -    row = manager._store._conn.execute(
    -        "SELECT role, content, seq, extra FROM messages WHERE session_key = ?",
    -        (session.key,),
    -    ).fetchone()
    -    assert row is not None
    -    assert tuple(row[:3]) == ("user", "正文", 0)
    -    assert row[3] == json.dumps(
    -        {"custom_first": "先", "custom_second": {"nested": "值"}},
    -        ensure_ascii=False,
    -    )
    -
    -
    -def test_session_manager_preserves_message_field_evaluation_order(
    -    tmp_path: Path,
    -) -> None:
    -    events: list[str] = []
    -
    -    class TraceMessage(dict[str, object]):
    -        def get(self, key: str, default: object = None) -> object:
    -            events.append(f"get:{key}")
    -            return super().get(key, default)
    -
    -        def items(self):
    -            events.append("items")
    -            return super().items()
    -
    -    manager = SessionManager(tmp_path)
    -    manager._store.persist_session = lambda *args, **kwargs: []
    -    fixed = datetime(2026, 7, 23, tzinfo=timezone.utc)
    -    session = Session("telegram:extra-order", created_at=fixed, updated_at=fixed)
    -    message = TraceMessage(
    -        role="user",
    -        content="正文",
    -        timestamp=fixed.isoformat(),
    -        custom="extra",
    -    )
    -
    -    manager._persist_session(session, [message], updated_at=fixed)
    -
    -    assert events == [
    -        "get:id",
    -        "get:timestamp",
    -        "get:content",
    -        "get:role",
    -        "get:tool_chain",
    -        "items",
    -    ]
    -
    -
    -def test_session_persistence_allocates_sequences_inside_transaction(
    -    tmp_path: Path,
    -) -> None:
    -    db_path = tmp_path / "sessions.db"
    -    store_a = SessionStore(db_path)
    -    store_b = SessionStore(db_path)
    -    key = "telegram:concurrent"
    -
    -    def persist(store: SessionStore, content: str) -> list[dict[str, Any]]:
    -        return store.persist_session(
    -            key,
    -            created_at="2026-07-13T00:00:00+00:00",
    -            updated_at="2026-07-13T00:00:01+00:00",
    -            metadata={},
    -            messages=[
    -                {
    -                    "role": "user",
    -                    "content": content,
    -                    "timestamp": "2026-07-13T00:00:01+00:00",
    -                    "extra": {},
    -                }
    -            ],
    -        )
    -
    -    try:
    -        with ThreadPoolExecutor(max_workers=2) as pool:
    -            futures = [
    -                pool.submit(persist, store_a, "来自 A"),
    -                pool.submit(persist, store_b, "来自 B"),
    -            ]
    -            rows = [future.result(timeout=5) for future in futures]
    -        messages = store_a.fetch_session_messages(key)
    -    finally:
    -        store_a.close()
    -        store_b.close()
    -
    -    assert {str(row[0]["id"]) for row in rows} == {
    -        f"{key}:0",
    -        f"{key}:1",
    -    }
    -    assert [str(message["id"]) for message in messages] == [
    -        f"{key}:0",
    -        f"{key}:1",
    -    ]
    -
    -
    -def test_session_store_reuses_existing_fts_without_rebuild(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -) -> None:
    -    db_path = tmp_path / "sessions.db"
    -    store = SessionStore(db_path)
    -    store.persist_session(
    -        "telegram:fts",
    -        created_at="2026-07-13T00:00:00+00:00",
    -        updated_at="2026-07-13T00:00:01+00:00",
    -        metadata={},
    -        messages=[
    -            {
    -                "role": "user",
    -                "content": "全文索引消息",
    -                "timestamp": "2026-07-13T00:00:01+00:00",
    -                "extra": {},
    -            }
    -        ],
    -    )
    -    store.close()
    -
    -    statements: list[str] = []
    -
    -    original_ensure_fts = SessionStore._ensure_fts
    -
    -    def trace_constructor_fts(instance: SessionStore) -> None:
    -        instance._conn.set_trace_callback(statements.append)
    -        original_ensure_fts(instance)
    -
    -    monkeypatch.setattr(SessionStore, "_ensure_fts", trace_constructor_fts)
    -    reopened = SessionStore(db_path)
    -
    -    assert reopened._has_fts is True
    -    assert not any("VALUES('rebuild')" in statement for statement in statements)
    -    assert reopened.search_messages("全文索引")[1] == 1
    -    reopened.close()
    -
    -
    -def test_session_store_rebuilds_fts_when_trigger_is_missing(tmp_path: Path) -> None:
    -    db_path = tmp_path / "sessions.db"
    -    store = SessionStore(db_path)
    -    store.persist_session(
    -        "telegram:fts-trigger",
    -        created_at="2026-07-13T00:00:00+00:00",
    -        updated_at="2026-07-13T00:00:01+00:00",
    -        metadata={},
    -        messages=[
    -            {
    -                "role": "user",
    -                "content": "触发器缺失后仍要检索",
    -                "timestamp": "2026-07-13T00:00:01+00:00",
    -                "extra": {},
    -            }
    -        ],
    -    )
    -    store._conn.execute("DROP TRIGGER messages_ai")
    -    store._conn.commit()
    -    statements: list[str] = []
    -    store._conn.set_trace_callback(statements.append)
    -
    -    store._ensure_fts()
    -
    -    assert any("VALUES('rebuild')" in statement for statement in statements)
    -    assert store.search_messages("触发器缺失")[1] == 1
    -    store.close()
    -
    -
    -def test_session_store_disables_fts_only_when_capability_is_missing(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    class _MissingFtsConnection:
    -        def execute(self, sql: str, _params: object = ()) -> object:
    -            if sql.startswith("SELECT name, sql"):
    -                return SimpleNamespace(fetchone=lambda: None)
    -            raise sqlite3.OperationalError("no such module: fts5")
    -
    -    store = SessionStore.__new__(SessionStore)
    -    store._conn = _MissingFtsConnection()  # type: ignore[assignment]
    -    store._has_fts = True
    -    store._closed = True
    -
    -    with caplog.at_level(logging.WARNING, logger="session.store"):
    -        store._ensure_fts()
    -
    -    assert store._has_fts is False
    -    assert "FTS5/trigram" in caplog.text
    -
    -
    -def test_session_store_reraises_non_capability_fts_errors() -> None:
    -    class _BrokenFtsConnection:
    -        def execute(self, _sql: str, _params: object = ()) -> object:
    -            raise sqlite3.OperationalError("database is locked")
    -
    -    store = SessionStore.__new__(SessionStore)
    -    store._conn = _BrokenFtsConnection()  # type: ignore[assignment]
    -    store._has_fts = True
    -    store._closed = True
    -
    -    with pytest.raises(sqlite3.OperationalError, match="database is locked"):
    -        store._ensure_fts()
    -
    -    assert store._has_fts is True
    -
    -
    -@pytest.mark.parametrize(
    -    ("column", "payload", "field"),
    -    [
    -        ("extra", '[["role", "spoofed"]]', "message extra"),
    -        ("extra", '{"role": "spoofed"}', "不得覆盖消息列字段"),
    -        ("tool_chain", '{"call": "invalid"}', "message tool_chain"),
    -        ("extra", "", "message extra"),
    -        ("tool_chain", "", "message tool_chain"),
    -        ("extra", '{"media": "path"}', "message media"),
    -        ("extra", '{"media": null}', "message media"),
    -        ("extra", '{"source_refs": ["bad"]}', "message source_refs"),
    -        ("tool_chain", '[{"calls": null}]', "message tool_chain"),
    -        ("tool_chain", "[null]", "message tool_chain"),
    -        ("tool_chain", '[{"calls": [null]}]', "message tool_chain"),
    -        ("tool_chain", '[{"calls": [{}]}]', "arguments 必须是 JSON object"),
    -        (
    -            "tool_chain",
    -            '[{"calls": [{"arguments": null}]}]',
    -            "arguments 必须是 JSON object",
    -        ),
    -        (
    -            "tool_chain",
    -            '[{"calls": [{"arguments": "{}"}]}]',
    -            "arguments 必须是 JSON object",
    -        ),
    -    ],
    -)
    -def test_session_store_rejects_invalid_message_json(
    -    tmp_path: Path,
    -    column: str,
    -    payload: str,
    -    field: str,
    -) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.persist_session(
    -        "telegram:json",
    -        created_at="2026-07-13T00:00:00+00:00",
    -        updated_at="2026-07-13T00:00:01+00:00",
    -        metadata={},
    -        messages=[
    -            {
    -                "role": "user",
    -                "content": "消息",
    -                "timestamp": "2026-07-13T00:00:01+00:00",
    -                "extra": {},
    -            }
    -        ],
    -    )
    -    store._conn.execute(
    -        f"UPDATE messages SET {column} = ? WHERE id = ?",
    -        (payload, "telegram:json:0"),
    -    )
    -    store._conn.commit()
    -
    -    with pytest.raises(ValueError, match=field):
    -        store.fetch_session_messages("telegram:json")
    -    store.close()
    -
    -
    -@pytest.mark.parametrize("payload", ['{"media": [}', '{"media": "path"}'])
    -def test_session_store_media_lookup_rejects_invalid_extra(
    -    tmp_path: Path,
    -    payload: str,
    -) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    try:
    -        store.persist_session(
    -            "telegram:media",
    -            created_at="2026-07-13T00:00:00+00:00",
    -            updated_at="2026-07-13T00:00:01+00:00",
    -            metadata={},
    -            messages=[
    -                {
    -                    "role": "user",
    -                    "content": "消息",
    -                    "timestamp": "2026-07-13T00:00:01+00:00",
    -                    "extra": {},
    -                }
    -            ],
    -        )
    -        store._conn.execute(
    -            "UPDATE messages SET extra = ? WHERE id = ?",
    -            (payload, "telegram:media:0"),
    -        )
    -        store._conn.commit()
    -
    -        with pytest.raises(ValueError, match="telegram:media:0"):
    -            store.media_path_exists(tmp_path / "path")
    -    finally:
    -        store.close()
    -
    -
    -@pytest.mark.parametrize(
    -    ("column", "payload", "field"),
    -    [("role", "system", "message role"), ("content", None, "message content")],
    -)
    -def test_session_store_rejects_invalid_message_columns(
    -    tmp_path: Path,
    -    column: str,
    -    payload: object,
    -    field: str,
    -) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    try:
    -        store.persist_session(
    -            "telegram:columns",
    -            created_at="2026-07-13T00:00:00+00:00",
    -            updated_at="2026-07-13T00:00:01+00:00",
    -            metadata={},
    -            messages=[
    -                {
    -                    "role": "user",
    -                    "content": "消息",
    -                    "timestamp": "2026-07-13T00:00:01+00:00",
    -                    "extra": {},
    -                }
    -            ],
    -        )
    -        store._conn.execute(
    -            f"UPDATE messages SET {column} = ? WHERE id = ?",
    -            (payload, "telegram:columns:0"),
    -        )
    -        store._conn.commit()
    -
    -        with pytest.raises(ValueError, match=field):
    -            store.fetch_session_messages("telegram:columns")
    -    finally:
    -        store.close()
    -
    -
    -def test_session_get_history_returns_empty_when_window_is_zero():
    -    session = Session("cli:1")
    -    session.add_message("user", "hello")
    -    session.add_message("assistant", "world")
    -
    -    assert session.get_history(max_messages=0) == []
    -
    -
    -def test_session_get_history_skips_cached_llm_frame_by_default():
    -    session = Session("cli:1")
    -    session.add_message("user", "old")
    -    session.add_message("assistant", "old reply")
    -    session.last_consolidated = 2
    -    user_content = "[当前消息时间: x]\nhello"
    -    session.add_message(
    -        "user",
    -        "hello",
    -        llm_context_frame='\n\n## retrieved_memory\n旧记忆',
    -        llm_user_content=user_content,
    -    )
    -    session.add_message("assistant", "world")
    -
    -    history = session.get_history(max_messages=1)
    -
    -    assert history == [
    -        {"role": "user", "content": user_content},
    -        {"role": "assistant", "content": "world"},
    -    ]
    -
    -
    -def test_session_get_history_replays_full_proactive_with_meta_frame():
    -    session = Session("cli:1")
    -    proactive_content = (
    -        "第一篇 TokenMem 介绍知识注入。\n\n"
    -        + "第二篇情景记忆包含较长正文。" * 30
    -        + "\n\n第三篇 HCG-RAG 使用模式约束因果图。"
    -    )
    -    assert len(proactive_content) > 360
    -    session.add_message(
    -        "assistant",
    -        proactive_content,
    -        proactive=True,
    -        source_refs=[
    -            {
    -                "source_name": "feed",
    -                "title": "标题",
    -                "url": "https://example.com/a",
    -            }
    -        ],
    -    )
    -
    -    history = session.get_history()
    -
    -    assert len(history) == 2
    -    assert history[0] == {
    -        "role": "assistant",
    -        "content": f"[主动推送] {proactive_content}",
    -    }
    -    assert history[1]["role"] == "user"
    -    content = str(history[1]["content"])
    -    assert is_context_frame(content)
    -    assert "recent_proactive_message_meta" in content
    -    assert "proactive_meta" in content
    -
    -
    -def test_session_get_history_allows_proactive_assistant_boundary():
    -    session = Session("cli:1")
    -    session.add_message("user", "old")
    -    session.add_message("assistant", "old reply")
    -    session.add_message("assistant", "主动消息", proactive=True)
    -    session.add_message("user", "刚才那个")
    -    session.last_consolidated = 2
    -
    -    history = session.get_history(max_messages=2)
    -
    -    assert history == [
    -        {"role": "assistant", "content": "[主动推送] 主动消息"},
    -        {"role": "user", "content": "刚才那个"},
    -    ]
    -
    -
    -def test_session_get_history_keeps_twenty_proactive_messages_before_reply():
    -    session = Session("mobile:fixture")
    -    for index in range(20):
    -        session.add_message(
    -            "assistant",
    -            f"主动消息 {index}",
    -            proactive=True,
    -            delivery_id=f"delivery-{index}",
    -            control_turn_id=f"wake-turn-{index}",
    -        )
    -    session.add_message(
    -        "user",
    -        "u",
    -        control_turn_id="passive-turn",
    -        turn_input_ordinal=0,
    -    )
    -    session.add_message(
    -        "assistant",
    -        "a",
    -        control_turn_id="passive-turn",
    -        turn_terminal=True,
    -        turn_input_count=1,
    -    )
    -
    -    history = session.get_history()
    -
    -    assert [message["content"] for message in history[:20]] == [
    -        f"[主动推送] 主动消息 {index}" for index in range(20)
    -    ]
    -    assert history[-2:] == [
    -        {"role": "user", "content": "u"},
    -        {"role": "assistant", "content": "a"},
    -    ]
    -
    -
    -def test_session_get_history_never_splits_explicit_multi_input_turn():
    -    session = Session("cli:multi-input")
    -    session.add_message("user", "old")
    -    session.add_message("assistant", "old reply")
    -    for ordinal, content in enumerate(("u1", "u2", "u3")):
    -        session.add_message(
    -            "user",
    -            content,
    -            control_turn_id="turn-1",
    -            turn_input_ordinal=ordinal,
    -        )
    -    session.add_message(
    -        "assistant",
    -        "final",
    -        control_turn_id="turn-1",
    -        turn_terminal=True,
    -        turn_input_count=3,
    -    )
    -
    -    expected = [
    -        {"role": "user", "content": "u1"},
    -        {"role": "user", "content": "u2"},
    -        {"role": "user", "content": "u3"},
    -        {"role": "assistant", "content": "final"},
    -    ]
    -    assert session.get_history(max_messages=1) == expected
    -
    -
    -def test_session_get_history_counts_logical_turn_and_proactive_as_units():
    -    session = Session("cli:logical-window")
    -    session.add_message("user", "old", control_turn_id="turn-old")
    -    session.add_message("assistant", "old reply", control_turn_id="turn-old")
    -    session.add_message("assistant", "主动提醒", proactive=True)
    -    for ordinal, content in enumerate(("u1", "u2", "u3")):
    -        session.add_message(
    -            "user",
    -            content,
    -            control_turn_id="turn-current",
    -            turn_input_ordinal=ordinal,
    -        )
    -    session.add_message(
    -        "assistant",
    -        "final",
    -        control_turn_id="turn-current",
    -        turn_terminal=True,
    -        turn_input_count=3,
    -    )
    -
    -    history = session.get_history(max_messages=2)
    -
    -    assert history[0] == {"role": "assistant", "content": "[主动推送] 主动提醒"}
    -    assert history[1:] == [
    -        {"role": "user", "content": "u1"},
    -        {"role": "user", "content": "u2"},
    -        {"role": "user", "content": "u3"},
    -        {"role": "assistant", "content": "final"},
    -    ]
    -
    -
    -def test_session_get_history_keeps_full_consolidated_tail():
    -    session = Session("cli:1")
    -    for i in range(5):
    -        session.add_message("user", f"u{i}")
    -
    -    history = session.get_history(max_messages=500)
    -
    -    assert history == [
    -        {"role": "user", "content": "u0"},
    -        {"role": "user", "content": "u1"},
    -        {"role": "user", "content": "u2"},
    -        {"role": "user", "content": "u3"},
    -        {"role": "user", "content": "u4"},
    -    ]
    -
    -
    -def test_session_get_history_skips_legacy_context_frame_by_default():
    -    session = Session("cli:1")
    -    session.add_message(
    -        "user",
    -        "hello",
    -        llm_context_frame="[SYSTEM_CONTEXT_FRAME]\n\n## context\n旧内容",
    -        llm_user_content="hello",
    -    )
    -
    -    history = session.get_history(max_messages=500)
    -
    -    assert history == [{"role": "user", "content": "hello"}]
    -
    -
    -def test_session_get_history_does_not_inject_inference_tag():
    -    session = Session("cli:1")
    -    session.add_message("user", "hello")
    -    session.add_message("assistant", "world")
    -
    -    history = session.get_history()
    -
    -    assert history[-1] == {"role": "assistant", "content": "world"}
    -
    -
    -def test_session_get_history_keeps_reasoning_content():
    -    session = Session("cli:1")
    -    session.add_message("user", "hello")
    -    session.add_message(
    -        "assistant",
    -        "world",
    -        reasoning_content="先想一下",
    -    )
    -    session.messages[-1]["tool_chain"] = [
    -        {
    -            "text": "",
    -            "reasoning_content": "准备调用工具",
    -            "calls": [
    -                {
    -                    "call_id": "call-1",
    -                    "name": "dummy",
    -                    "arguments": {},
    -                    "result": "ok",
    -                }
    -            ],
    -        }
    -    ]
    -
    -    history = session.get_history()
    -
    -    assert history[1]["reasoning_content"] == "准备调用工具"
    -    assert history[-1]["reasoning_content"] == "先想一下"
    -
    -
    -def test_session_get_history_keeps_short_tool_results_after_consolidation_tail():
    -    session = Session("cli:1")
    -    session.last_consolidated = 0
    -    for i in range(3):
    -        session.add_message("user", f"u{i}")
    -        session.add_message("assistant", f"a{i}")
    -        session.messages[-1]["tool_chain"] = [
    -            {
    -                "text": "",
    -                "calls": [
    -                    {
    -                        "call_id": f"call-{i}",
    -                        "name": "dummy",
    -                        "arguments": {},
    -                        "result": f"result-{i}",
    -                    }
    -                ],
    -            }
    -        ]
    -
    -    history = session.get_history(max_messages=500)
    -    tool_contents = [m["content"] for m in history if m.get("role") == "tool"]
    -
    -    assert tool_contents == ["result-0", "result-1", "result-2"]
    -
    -
    -def test_session_get_history_truncates_long_tool_results_in_middle():
    -    session = Session("cli:1")
    -    long_result = "head-" + "x" * (_TOOL_RESULT_CHAR_BUDGET + 200) + "-tail"
    -    session.add_message("user", "u")
    -    session.add_message("assistant", "a")
    -    session.messages[-1]["tool_chain"] = [
    -        {
    -            "text": "",
    -            "calls": [
    -                {
    -                    "call_id": "call-1",
    -                    "name": "dummy",
    -                    "arguments": {},
    -                    "result": long_result,
    -                }
    -            ],
    -        }
    -    ]
    -
    -    history = session.get_history()
    -    tool_content = cast(
    -        str,
    -        next(m["content"] for m in history if m.get("role") == "tool"),
    -    )
    -
    -    assert tool_content.startswith("Total output lines: 1\n\nhead-")
    -    assert "chars truncated" in tool_content
    -    assert tool_content.endswith("-tail")
    -    assert len(tool_content) < len(long_result)
    -
    -
    -def test_session_history_does_not_mask_non_oserror_media_read(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -) -> None:
    -    image = tmp_path / "image.png"
    -    image.write_bytes(b"image")
    -
    -    def fail_read(_path: Path) -> bytes:
    -        raise ValueError("损坏的媒体读取状态")
    -
    -    monkeypatch.setattr(Path, "read_bytes", fail_read)
    -    session = Session("cli:media")
    -    session.add_message("user", "查看图片", media=[str(image)])
    -
    -    with pytest.raises(ValueError, match="损坏的媒体读取状态"):
    -        session.get_history()
    diff --git a/tests/test_longmemeval_metrics.py b/tests/test_longmemeval_metrics.py
    deleted file mode 100644
    index 52f4f1081..000000000
    --- a/tests/test_longmemeval_metrics.py
    +++ /dev/null
    @@ -1,23 +0,0 @@
    -from __future__ import annotations
    -
    -import pytest
    -
    -from agent.plugin_composition import ModelRequest
    -from eval.longmemeval.metrics import judge_answer
    -
    -
    -class _BrokenJudge:
    -    async def complete(self, request: ModelRequest):
    -        del request
    -        raise AssertionError("broken judge contract")
    -
    -
    -@pytest.mark.asyncio
    -async def test_judge_internal_failure_is_not_scored_as_wrong() -> None:
    -    with pytest.raises(AssertionError, match="broken judge contract"):
    -        await judge_answer(
    -            _BrokenJudge(),  # type: ignore[arg-type]
    -            question="question",
    -            gold="gold",
    -            predicted="predicted",
    -        )
    diff --git a/tests/test_loop_tool_visibility.py b/tests/test_loop_tool_visibility.py
    deleted file mode 100644
    index fbfa82fc6..000000000
    --- a/tests/test_loop_tool_visibility.py
    +++ /dev/null
    @@ -1,606 +0,0 @@
    -"""
    -tool_search 可见性机制 + LRU 回归测试。
    -
    -覆盖场景:
    -- tool_search_enabled=True 时非存在工具被拦截
    -- tool_search_enabled=True 时存在但不可见的工具自动解锁
    -- tool_search 调用结果正确扩展 visible_names
    -- LRU 容量上限 5,超出时淘汰最久未用
    -- 最近使用的工具刷新 LRU 顺序(不被淘汰)
    -- always_on 工具不写入 LRU
    -- preloaded 工具在下一请求中直接可见
    -"""
    -
    -import asyncio
    -import json
    -from collections import OrderedDict
    -from pathlib import Path
    -from typing import Any, cast
    -from unittest.mock import MagicMock
    -
    -from agent.looping.core import AgentLoop
    -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps, LLMConfig
    -from agent.context import ContextBuilder
    -from agent.control.context import running_turn_id
    -from bus.queue import MessageBus
    -from core.error_context import current_session_key
    -
    -import pytest
    -
    -from agent.plugin_composition import LLMResponse, ToolCall
    -from agent.tools.base import Tool
    -from agent.tools.registry import ToolRegistry
    -from agent.tools.tool_search import ToolSearchTool
    -from tests.memory_fakes import FakeMemoryEngine
    -from tests.provider_fakes import ProviderContextBudgetStub
    -from tests.compaction_fakes import run_test_agent_loop
    -
    -# ── 工具桩 ────────────────────────────────────────────────────────────────────
    -
    -
    -class _DummyTool(Tool):
    -    def __init__(self, name: str) -> None:
    -        self._name = name
    -        self.calls: list[dict] = []
    -
    -    @property
    -    def name(self) -> str:
    -        return self._name
    -
    -    @property
    -    def description(self) -> str:
    -        return f"dummy tool {self._name}"
    -
    -    @property
    -    def parameters(self) -> dict:
    -        return {"type": "object", "properties": {}}
    -
    -    async def execute(self, **kwargs: Any) -> str:
    -        self.calls.append(kwargs)
    -        return f"ok:{self._name}"
    -
    -
    -class _FakeProvider(ProviderContextBudgetStub):
    -    def __init__(self, responses: list[LLMResponse]) -> None:
    -        self._responses = list(responses)
    -
    -    async def chat(self, **kwargs: Any) -> LLMResponse:
    -        if not self._responses:
    -            raise AssertionError("provider.chat called more times than expected")
    -        return self._responses.pop(0)
    -
    -
    -# ── 工厂 ──────────────────────────────────────────────────────────────────────
    -
    -
    -def _make_loop(
    -    tmp_path: Path,
    -    provider: _FakeProvider,
    -    registry: ToolRegistry,
    -    tool_search_enabled: bool = True,
    -) -> AgentLoop:
    -    loop = AgentLoop(
    -        AgentLoopDeps(
    -            bus=MessageBus(),
    -            tools=registry,
    -            session_manager=MagicMock(),
    -            workspace=tmp_path,
    -            context=ContextBuilder(tmp_path),
    -        ),
    -        AgentLoopConfig(
    -            llm=LLMConfig(max_iterations=10, tool_search_enabled=tool_search_enabled)
    -        ),
    -    )
    -    return loop
    -
    -
    -def _base_registry() -> ToolRegistry:
    -    """只含 tool_search 的最小 registry。"""
    -    reg = ToolRegistry()
    -    reg.register(ToolSearchTool(reg), always_on=True, risk="read-only")
    -    return reg
    -
    -
    -# ── 可见性 / 拦截测试 ─────────────────────────────────────────────────────────
    -
    -
    -class TestVisibilityGuard:
    -    def test_nonexistent_tool_is_blocked(self, tmp_path):
    -        """完全不在 registry 里的工具名 → 拦截,不执行,返回错误消息给模型。"""
    -        reg = _base_registry()
    -        provider = _FakeProvider(
    -            [
    -                LLMResponse(content="", tool_calls=[ToolCall("c1", "ghost_tool", {})]),
    -                LLMResponse(content="ok", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, tools_used, _, _, _ = asyncio.run(
    -            run_test_agent_loop(loop, provider, [{"role": "user", "content": "test"}])
    -        )
    -
    -        assert final == "ok"
    -        assert "ghost_tool" not in tools_used  # 被拦截,不计入 tools_used
    -
    -    def test_deferred_tool_direct_call_blocked_with_select_hint(self, tmp_path):
    -        """在 registry 里但不在 visible_names 里的工具(deferred)直接调用
    -        → 不执行,返回 select: 引导错误,模型收到后给出最终回复。"""
    -        reg = _base_registry()
    -        hidden = _DummyTool("hidden_tool")
    -        reg.register(hidden)  # 不设 always_on → deferred
    -
    -        provider = _FakeProvider(
    -            [
    -                LLMResponse(content="", tool_calls=[ToolCall("c1", "hidden_tool", {})]),
    -                LLMResponse(content="done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, tools_used, tool_chain, _, _ = asyncio.run(
    -            run_test_agent_loop(loop, provider, [{"role": "user", "content": "test"}])
    -        )
    -
    -        assert final == "done"
    -        assert "hidden_tool" not in tools_used  # 未执行,不计入 tools_used
    -        assert len(hidden.calls) == 0  # 工具实体未被调用
    -
    -        # 第一轮 tool_chain 应有 select: 引导错误
    -        calls = tool_chain[0]["calls"] if tool_chain else []
    -        hidden_call = next((c for c in calls if c["name"] == "hidden_tool"), None)
    -        assert hidden_call is not None
    -        assert "select:" in hidden_call["result"]
    -
    -    def test_tool_search_enabled_false_exposes_all_tools(self, tmp_path):
    -        """tool_search_enabled=False 时全量暴露,hidden tool 直接可用。"""
    -        reg = _base_registry()
    -        hidden = _DummyTool("hidden_tool")
    -        reg.register(hidden)
    -
    -        provider = _FakeProvider(
    -            [
    -                LLMResponse(content="", tool_calls=[ToolCall("c1", "hidden_tool", {})]),
    -                LLMResponse(content="done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg, tool_search_enabled=False)
    -
    -        _, tools_used, _, _, _ = asyncio.run(
    -            run_test_agent_loop(loop, provider, [{"role": "user", "content": "test"}])
    -        )
    -
    -        assert "hidden_tool" in tools_used
    -
    -    def test_tool_search_result_unlocks_target_tool(self, tmp_path):
    -        """调用 tool_search 后,返回结果里的工具名加入 visible_names。"""
    -        reg = _base_registry()
    -        target = _DummyTool("target_tool")
    -        reg.register(target)
    -
    -        # tool_search 直接返回匹配结果(模拟 registry.search 找到了 target_tool)
    -        tool_search_result = json.dumps(
    -            {
    -                "matched": [
    -                    {
    -                        "name": "target_tool",
    -                        "summary": "...",
    -                        "why_matched": [],
    -                        "key_params": [],
    -                        "tags": [],
    -                        "risk": "read-only",
    -                    }
    -                ]
    -            },
    -            ensure_ascii=False,
    -        )
    -
    -        provider = _FakeProvider(
    -            [
    -                # 第 1 轮:调用 tool_search
    -                LLMResponse(
    -                    content="",
    -                    tool_calls=[ToolCall("s1", "tool_search", {"query": "target"})],
    -                ),
    -                # 第 2 轮:调用解锁后的 target_tool
    -                LLMResponse(content="", tool_calls=[ToolCall("t1", "target_tool", {})]),
    -                # 第 3 轮:返回最终结果
    -                LLMResponse(content="all done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, tools_used, _, _, _ = asyncio.run(
    -            run_test_agent_loop(
    -                loop, provider, [{"role": "user", "content": "use target"}]
    -            )
    -        )
    -
    -        assert "target_tool" in tools_used
    -        assert len(target.calls) == 1
    -        assert final == "all done"
    -
    -    def test_visible_names_starts_with_only_always_on(self, tmp_path):
    -        """tool_search_enabled=True 时,第一次 LLM 调用只传 always_on 工具 schema。"""
    -        reg = _base_registry()
    -        hidden = _DummyTool("hidden_tool")
    -        reg.register(hidden)
    -
    -        schemas_seen: list[list[str]] = []
    -
    -        class _CapturingProvider(ProviderContextBudgetStub):
    -            _responses = [LLMResponse(content="done", tool_calls=[])]
    -
    -            async def chat(self, **kwargs: Any) -> LLMResponse:
    -                schemas_seen.append(
    -                    [t["function"]["name"] for t in (kwargs.get("tools") or [])]
    -                )
    -                return self._responses.pop(0)
    -
    -        provider = cast(Any, _CapturingProvider())
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        asyncio.run(
    -            run_test_agent_loop(loop, provider, [{"role": "user", "content": "test"}])
    -        )
    -
    -        assert schemas_seen, "provider.chat was never called"
    -        first_call_tools = schemas_seen[0]
    -        assert "tool_search" in first_call_tools
    -        assert "hidden_tool" not in first_call_tools
    -
    -    def test_unlocked_schema_appends_after_always_on(self, tmp_path):
    -        reg = ToolRegistry()
    -        hidden = _DummyTool("early_hidden")
    -        reg.register(hidden)
    -        reg.register(ToolSearchTool(reg), always_on=True, risk="read-only")
    -
    -        schemas_seen: list[list[str]] = []
    -        messages_seen: list[list[dict[str, Any]]] = []
    -
    -        class _CapturingProvider(_FakeProvider):
    -            async def chat(self, **kwargs: Any) -> LLMResponse:
    -                schemas_seen.append(
    -                    [t["function"]["name"] for t in (kwargs.get("tools") or [])]
    -                )
    -                messages_seen.append(list(kwargs.get("messages") or []))
    -                return await super().chat(**kwargs)
    -
    -        provider = _CapturingProvider(
    -            [
    -                LLMResponse(
    -                    content="",
    -                    tool_calls=[
    -                        ToolCall("s1", "tool_search", {"query": "select:early_hidden"})
    -                    ],
    -                ),
    -                LLMResponse(
    -                    content="", tool_calls=[ToolCall("h1", "early_hidden", {})]
    -                ),
    -                LLMResponse(content="done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, tools_used, _, _, _ = asyncio.run(
    -            run_test_agent_loop(
    -                loop, provider, [{"role": "user", "content": "use hidden"}]
    -            )
    -        )
    -
    -        assert final == "done"
    -        assert "early_hidden" in tools_used
    -        assert schemas_seen[0] == ["tool_search"]
    -        assert schemas_seen[1] == ["tool_search", "early_hidden"]
    -        second_call_text = "\n".join(
    -            str(message.get("content") or "") for message in messages_seen[1]
    -        )
    -        assert "当前工具状态" not in second_call_text
    -
    -    def test_provider_tool_limit_keeps_always_on_and_prioritizes_new_unlock(
    -        self, tmp_path
    -    ):
    -        reg = ToolRegistry()
    -        reg.register(ToolSearchTool(reg), always_on=True, risk="read-only")
    -        for name in ("always_a", "always_b"):
    -            reg.register(_DummyTool(name), always_on=True)
    -        reg.register(_DummyTool("old_preload"))
    -        reg.register(_DummyTool("selected_tool"))
    -
    -        schemas_seen: list[list[str]] = []
    -
    -        class _LimitedProvider(_FakeProvider):
    -            max_tool_schemas = 4
    -
    -            async def chat(self, **kwargs: Any) -> LLMResponse:
    -                schemas_seen.append(
    -                    [tool["function"]["name"] for tool in kwargs.get("tools") or []]
    -                )
    -                return await super().chat(**kwargs)
    -
    -        provider = _LimitedProvider(
    -            [
    -                LLMResponse(
    -                    content="",
    -                    tool_calls=[
    -                        ToolCall("s1", "tool_search", {"query": "select:selected_tool"})
    -                    ],
    -                ),
    -                LLMResponse(content="done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, _, _, _, _ = asyncio.run(
    -            run_test_agent_loop(
    -                loop,
    -                provider,
    -                [{"role": "user", "content": "use selected"}],
    -                preloaded_tools={"old_preload"},
    -            )
    -        )
    -
    -        assert final == "done"
    -        assert schemas_seen[0] == [
    -            "tool_search",
    -            "always_a",
    -            "always_b",
    -            "old_preload",
    -        ]
    -        assert schemas_seen[1] == [
    -            "tool_search",
    -            "always_a",
    -            "always_b",
    -            "selected_tool",
    -        ]
    -        assert all(len(names) <= 4 for names in schemas_seen)
    -        deferred = reg.get_deferred_names(visible=set(schemas_seen[0]))
    -        assert "selected_tool" in deferred["builtin"]
    -
    -    def test_provider_tool_limit_projects_overfull_always_on_catalog(self, tmp_path):
    -        reg = ToolRegistry()
    -        reg.register(ToolSearchTool(reg), always_on=True, risk="read-only")
    -        reg.register(_DummyTool("always_a"), always_on=True)
    -        reg.register(_DummyTool("always_b"), always_on=True)
    -        reg.register(_DummyTool("recent_preload"))
    -
    -        schemas_seen: list[list[str]] = []
    -
    -        class _LimitedProvider(_FakeProvider):
    -            max_tool_schemas = 2
    -
    -            async def chat(self, **kwargs: Any) -> LLMResponse:
    -                schemas_seen.append(
    -                    [tool["function"]["name"] for tool in kwargs.get("tools") or []]
    -                )
    -                return await super().chat(**kwargs)
    -
    -        provider = _LimitedProvider([LLMResponse(content="done", tool_calls=[])])
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        final, _, _, _, _ = asyncio.run(
    -            run_test_agent_loop(
    -                loop,
    -                provider,
    -                [{"role": "user", "content": "hello"}],
    -                preloaded_tools={"recent_preload"},
    -            )
    -        )
    -
    -        assert final == "done"
    -        assert schemas_seen == [["tool_search", "recent_preload"]]
    -        assert (
    -            "always_a"
    -            in reg.get_deferred_names(visible=set(schemas_seen[0]))["builtin"]
    -        )
    -
    -    def test_search_unlock_replaces_overfull_always_on_tool(self, tmp_path):
    -        reg = ToolRegistry()
    -        reg.register(ToolSearchTool(reg), always_on=True, risk="read-only")
    -        reg.register(_DummyTool("always_a"), always_on=True)
    -        reg.register(_DummyTool("always_b"), always_on=True)
    -        selected = _DummyTool("selected_tool")
    -        overflow = _DummyTool("overflow_tool")
    -        reg.register(selected, requires_turn_search=True)
    -        reg.register(overflow, requires_turn_search=True)
    -
    -        schemas_seen: list[list[str]] = []
    -
    -        class _LimitedProvider(_FakeProvider):
    -            max_tool_schemas = 2
    -
    -            async def chat(self, **kwargs: Any) -> LLMResponse:
    -                schemas_seen.append(
    -                    [tool["function"]["name"] for tool in kwargs.get("tools") or []]
    -                )
    -                return await super().chat(**kwargs)
    -
    -        provider = _LimitedProvider(
    -            [
    -                LLMResponse(
    -                    content="",
    -                    tool_calls=[
    -                        ToolCall(
    -                            "s1",
    -                            "tool_search",
    -                            {"query": "select:selected_tool,overflow_tool"},
    -                        )
    -                    ],
    -                ),
    -                LLMResponse(
    -                    content="",
    -                    tool_calls=[ToolCall("t1", "selected_tool", {})],
    -                ),
    -                LLMResponse(content="done", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider, reg)
    -
    -        turn_token = running_turn_id.set("turn:search-cap")
    -        session_token = current_session_key.set("programmatic:search-cap")
    -        scope = reg.begin_turn_search_scope(
    -            turn_id="turn:search-cap",
    -            session_key="programmatic:search-cap",
    -            attempt=0,
    -        )
    -        try:
    -            final, tools_used, tool_chain, _, _ = asyncio.run(
    -                run_test_agent_loop(
    -                    loop,
    -                    provider,
    -                    [{"role": "user", "content": "use selected"}],
    -                )
    -            )
    -            with pytest.raises(RuntimeError, match="必须在当前 turn"):
    -                asyncio.run(reg.execute("overflow_tool", {}, raise_errors=True))
    -        finally:
    -            reg.end_turn_search_scope(scope)
    -            current_session_key.reset(session_token)
    -            running_turn_id.reset(turn_token)
    -
    -        assert final == "done"
    -        assert schemas_seen == [
    -            ["tool_search", "always_a"],
    -            ["tool_search", "selected_tool"],
    -            ["tool_search", "selected_tool"],
    -        ]
    -        assert tools_used == ["tool_search", "selected_tool"]
    -        assert len(selected.calls) == 1
    -        assert len(overflow.calls) == 0
    -        search_result = json.loads(tool_chain[0]["calls"][0]["result"])
    -        assert search_result["unlocked"] == ["selected_tool"]
    -        assert search_result["capacity_limited"] == ["overflow_tool"]
    -
    -
    -# ── LRU 测试 ──────────────────────────────────────────────────────────────────
    -
    -
    -class TestLRUCache:
    -    def _make_loop_for_lru(self, tmp_path: Path) -> AgentLoop:
    -        reg = _base_registry()
    -        # 注册 10 个非核心工具
    -        for i in range(10):
    -            reg.register(_DummyTool(f"tool_{i}"))
    -        return _make_loop(tmp_path, cast(Any, _FakeProvider([])), reg)
    -
    -    def test_lru_capacity_5(self, tmp_path):
    -        """写入 6 个工具后,LRU 只保留最新 5 个。"""
    -        loop = self._make_loop_for_lru(tmp_path)
    -        loop._tool_discovery.update(
    -            "s1", [f"tool_{i}" for i in range(6)], loop.tools.get_always_on_names()
    -        )
    -
    -        lru = loop._tool_discovery._unlocked["s1"]
    -        assert len(lru) == 5
    -        # tool_0 是最早写入的,应被淘汰
    -        assert "tool_0" not in lru
    -        assert "tool_5" in lru
    -
    -    def test_lru_evicts_oldest_first(self, tmp_path):
    -        """容量满后,最久未使用的工具先被淘汰。"""
    -        loop = self._make_loop_for_lru(tmp_path)
    -        # 写入 5 个(满)
    -        loop._tool_discovery.update(
    -            "s1",
    -            ["tool_0", "tool_1", "tool_2", "tool_3", "tool_4"],
    -            loop.tools.get_always_on_names(),
    -        )
    -        # 再加 1 个 → tool_0 应被淘汰
    -        loop._tool_discovery.update("s1", ["tool_5"], loop.tools.get_always_on_names())
    -
    -        lru = loop._tool_discovery._unlocked["s1"]
    -        assert "tool_0" not in lru
    -        assert "tool_5" in lru
    -
    -    def test_lru_refresh_on_reuse(self, tmp_path):
    -        """重复使用某工具会刷新其在 LRU 中的位置,不被淘汰。"""
    -        loop = self._make_loop_for_lru(tmp_path)
    -        # 写入 5 个(满)
    -        loop._tool_discovery.update(
    -            "s1",
    -            ["tool_0", "tool_1", "tool_2", "tool_3", "tool_4"],
    -            loop.tools.get_always_on_names(),
    -        )
    -        # 重新使用 tool_0(刷到末尾)
    -        loop._tool_discovery.update("s1", ["tool_0"], loop.tools.get_always_on_names())
    -        # 再加 1 个 → tool_1(最久未用)应被淘汰,而非 tool_0
    -        loop._tool_discovery.update("s1", ["tool_5"], loop.tools.get_always_on_names())
    -
    -        lru = loop._tool_discovery._unlocked["s1"]
    -        assert "tool_0" in lru  # 刚被刷新,安全
    -        assert "tool_1" not in lru  # 最久未用,被淘汰
    -        assert "tool_5" in lru
    -
    -    def test_always_on_tools_not_in_lru(self, tmp_path):
    -        """always_on 工具不应写入 LRU。"""
    -        reg = _base_registry()
    -        reg.register(_DummyTool("always_tool"), always_on=True)
    -        reg.register(_DummyTool("normal_tool"))
    -        loop = _make_loop(tmp_path, cast(Any, _FakeProvider([])), reg)
    -
    -        loop._tool_discovery.update(
    -            "s1",
    -            ["always_tool", "tool_search", "normal_tool"],
    -            loop.tools.get_always_on_names(),
    -        )
    -
    -        lru = loop._tool_discovery._unlocked.get("s1", {})
    -        assert "always_tool" not in lru
    -        assert "tool_search" not in lru
    -        assert "normal_tool" in lru
    -
    -    def test_lru_preloaded_on_next_request(self, tmp_path):
    -        """上一请求写入 LRU 的工具,下一请求应出现在 preloaded 中。"""
    -        reg = _base_registry()
    -        target = _DummyTool("remembered_tool")
    -        reg.register(target)
    -
    -        # 第一请求:调用 remembered_tool(触发 auto-unlock + LRU 写入)
    -        provider1 = _FakeProvider(
    -            [
    -                LLMResponse(
    -                    content="", tool_calls=[ToolCall("c1", "remembered_tool", {})]
    -                ),
    -                LLMResponse(content="done1", tool_calls=[]),
    -            ]
    -        )
    -        loop = _make_loop(tmp_path, provider1, reg)
    -
    -        asyncio.run(
    -            run_test_agent_loop(
    -                loop,
    -                provider1,
    -                [{"role": "user", "content": "first"}],
    -                preloaded_tools=set(),
    -            )
    -        )
    -        # 手动模拟 _run_with_safety_retry 的 LRU 写入
    -        loop._tool_discovery.update(
    -            "session1", ["remembered_tool"], loop.tools.get_always_on_names()
    -        )
    -
    -        # 验证 LRU 已记录
    -        assert "remembered_tool" in loop._tool_discovery._unlocked.get("session1", {})
    -
    -        # 第二请求:preloaded 应包含该工具
    -        preloaded = set(loop._tool_discovery._unlocked["session1"].keys())
    -        assert "remembered_tool" in preloaded
    -
    -    def test_lru_independent_per_session(self, tmp_path):
    -        """不同 session_key 的 LRU 互相独立。"""
    -        loop = self._make_loop_for_lru(tmp_path)
    -        loop._tool_discovery.update(
    -            "session_a", ["tool_0", "tool_1"], loop.tools.get_always_on_names()
    -        )
    -        loop._tool_discovery.update(
    -            "session_b", ["tool_2", "tool_3"], loop.tools.get_always_on_names()
    -        )
    -
    -        assert set(loop._tool_discovery._unlocked["session_a"].keys()) == {
    -            "tool_0",
    -            "tool_1",
    -        }
    -        assert set(loop._tool_discovery._unlocked["session_b"].keys()) == {
    -            "tool_2",
    -            "tool_3",
    -        }
    diff --git a/tests/test_main_lightweight_commands.py b/tests/test_main_lightweight_commands.py
    deleted file mode 100644
    index ede0ea86c..000000000
    --- a/tests/test_main_lightweight_commands.py
    +++ /dev/null
    @@ -1,208 +0,0 @@
    -from __future__ import annotations
    -
    -import os
    -import sqlite3
    -import subprocess
    -import sys
    -import tomllib
    -from pathlib import Path
    -
    -from agent.persona import read_default_veda
    -
    -_PROJECT_ROOT = Path(__file__).parents[1]
    -
    -
    -def test_init_records_yoyo_origin_in_workspace_ledger(tmp_path: Path) -> None:
    -    config_path = tmp_path / "config.toml"
    -    workspace = tmp_path / "workspace"
    -
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "init",
    -            "--config",
    -            str(config_path),
    -            "--workspace",
    -            str(workspace),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        stdout=subprocess.PIPE,
    -        stderr=subprocess.PIPE,
    -        text=True,
    -        check=False,
    -    )
    -
    -    assert result.returncode == 0, result.stdout + result.stderr
    -    ledger = workspace / "migrations.sqlite3"
    -    connection = sqlite3.connect(ledger)
    -    try:
    -        applied = connection.execute(
    -            "SELECT migration_id FROM _yoyo_migration"
    -        ).fetchall()
    -    finally:
    -        connection.close()
    -    assert applied == [
    -        ("20260802_01_yoyo_origin",),
    -        ("20260805_01_akasha_sparse_index_v9",),
    -        ("20260807_01_model_registry_database",),
    -        ("20260807_01_session_context_compaction_ledger",),
    -        ("20260807_02_embedding_model_registry",),
    -        ("20260808_01_restore_migrated_reasoning_efforts",),
    -        ("20260808_01_session_mutation_audits",),
    -        ("20260808_02_correct_opencode_go_variants",),
    -        ("20260808_02_session_compaction_prepares",),
    -        ("20260808_04_session_compaction_source_plan_digest",),
    -        ("20260808_05_activate_session_compaction_cursor",),
    -        ("20260808_03_remove_compaction_trigger",),
    -        ("20260808_06_retire_legacy_context_state",),
    -        ("20260817_01_akasha_sparse_index_v10",),
    -        ("20260823_01_retire_legacy_toolset_wiring",),
    -        ("20260825_01_migrate_proactive_delivery_target",),
    -        ("20260826_01_migrate_turn_effects",),
    -        ("20260825_02_select_akasha_embedding_plugin",),
    -        ("20260826_02_backfill_akasha_message_embeddings",),
    -        ("20260826_03_unify_akashic_channel_identity",),
    -        ("20260827_01_normalize_session_timestamps",),
    -        ("20260827_02_migrate_legacy_mobile_client_ids",),
    -        ("20260828_01_migrate_eventmail_state",),
    -        ("20260828_02_add_wake_content_scores",),
    -        ("20260829_01_backfill_plugin_programmatic_effects",),
    -        ("20260829_02_backfill_explicit_programmatic_effects",),
    -        ("20260829_03_retire_core_model_config",),
    -        ("20260831_01_migrate_compaction_plugin_config",),
    -    ]
    -    assert not config_path.with_name("config.toml.migration-cursor").exists()
    -
    -
    -def test_veda_reset_runs_before_agent_runtime_and_preserves_original_bytes(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    veda = workspace / "memory/VEDA.md"
    -    veda.parent.mkdir(parents=True)
    -    original = b"\xffbroken"
    -    veda.write_bytes(original)
    -
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "veda-reset",
    -            "--workspace",
    -            str(workspace),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -
    -    output = result.stdout + result.stderr
    -    assert result.returncode == 0, output
    -    assert veda.read_text(encoding="utf-8").strip() == read_default_veda()
    -    backups = list((workspace / "memory/veda-backups").glob("*/VEDA.md"))
    -    assert len(backups) == 1
    -    assert backups[0].read_bytes() == original
    -    assert "原内容 sha256=" in output
    -    assert "apscheduler" not in output
    -
    -
    -def test_veda_reset_reports_noop_without_creating_backup(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -
    -    first = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "veda-reset",
    -            "--workspace",
    -            str(workspace),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -    second = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "veda-reset",
    -            "--workspace",
    -            str(workspace),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -
    -    assert first.returncode == 0, first.stdout + first.stderr
    -    assert second.returncode == 0, second.stdout + second.stderr
    -    assert "Veda 已是默认内容" in second.stdout
    -    assert not (workspace / "memory/veda-backups").exists()
    -
    -
    -def test_help_lists_veda_reset() -> None:
    -    result = subprocess.run(
    -        [sys.executable, str(_PROJECT_ROOT / "main.py"), "--help"],
    -        cwd=_PROJECT_ROOT,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -
    -    assert result.returncode == 0, result.stdout + result.stderr
    -    assert "veda-reset" in result.stdout
    -
    -
    -def test_plugin_toggle_accepts_builtin_plugin_id(tmp_path: Path) -> None:
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    manifest = plugin_home / "manifest.toml"
    -    manifest.write_text(
    -        '[plugins]\n\n[plugins."computer"]\nenabled = true\n',
    -        encoding="utf-8",
    -    )
    -    environment = {**os.environ, "AKASHIC_PLUGIN_HOME": str(plugin_home)}
    -
    -    disabled = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "plugin-disable",
    -            "computer",
    -            "--workspace",
    -            str(tmp_path / "workspace"),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        env=environment,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -    assert disabled.returncode == 0, disabled.stdout + disabled.stderr
    -    assert tomllib.loads(manifest.read_text(encoding="utf-8"))["plugins"]["computer"][
    -        "enabled"
    -    ] is False
    -
    -    enabled = subprocess.run(
    -        [
    -            sys.executable,
    -            str(_PROJECT_ROOT / "main.py"),
    -            "plugin-enable",
    -            "computer",
    -            "--workspace",
    -            str(tmp_path / "workspace"),
    -        ],
    -        cwd=_PROJECT_ROOT,
    -        env=environment,
    -        capture_output=True,
    -        text=True,
    -        check=False,
    -    )
    -    assert enabled.returncode == 0, enabled.stdout + enabled.stderr
    -    assert tomllib.loads(manifest.read_text(encoding="utf-8"))["plugins"]["computer"][
    -        "enabled"
    -    ] is True
    diff --git a/tests/test_markdown_memory_plugin.py b/tests/test_markdown_memory_plugin.py
    deleted file mode 100644
    index 8b0e0ce54..000000000
    --- a/tests/test_markdown_memory_plugin.py
    +++ /dev/null
    @@ -1,421 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import json
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.lifecycle.types import PromptRenderCtx
    -from agent.prompting.section_names import (
    -    LONG_TERM_PROFILE_SECTION,
    -    RETRIEVED_MEMORY_SECTION,
    -    SELF_PROFILE_SECTION,
    -)
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -from session.manager import SessionManager
    -from plugins.markdown_memory import plugin as markdown_plugin
    -from plugins.markdown_memory.plugin import (
    -    _inject_profiles,
    -    _migrate_pending,
    -    _prepare_draft,
    -    _source_text,
    -    _validate_memory,
    -    _validate_preserved_bullets,
    -    _validate_self,
    -)
    -from plugins.markdown_memory.store import (
    -    DEFAULT_SELF_MD,
    -    MarkdownProfileStore,
    -    content_digest,
    -)
    -
    -
    -def _store(tmp_path: Path) -> MarkdownProfileStore:
    -    return MarkdownProfileStore(
    -        tmp_path / "memory/MEMORY.md",
    -        tmp_path / "memory/SELF.md",
    -        tmp_path / "memory/markdown-profile-writes.db",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_prompt_profiles_are_ordinary_sections(tmp_path: Path) -> None:
    -    store = _store(tmp_path)
    -    store.memory_path.write_text("# 用户长期记忆\n\n## 用户事实\n- 花月使用 Akashic\n\n## 用户偏好\n- 简洁\n\n## 用户明确要求长期记住的关键内容\n- 无\n", encoding="utf-8")
    -    event = cast(
    -        PromptRenderCtx,
    -        SimpleNamespace(disabled_sections=set(), system_sections_bottom=[]),
    -    )
    -
    -    await _inject_profiles(event, store)
    -
    -    assert [section.name for section in event.system_sections_bottom] == [
    -        "self_model",
    -        "long_term_memory",
    -    ]
    -    assert DEFAULT_SELF_MD.strip() in event.system_sections_bottom[0].content
    -    assert "花月使用 Akashic" in event.system_sections_bottom[1].content
    -
    -
    -@pytest.mark.asyncio
    -async def test_prompt_profile_disable_is_source_neutral(tmp_path: Path) -> None:
    -    store = _store(tmp_path)
    -    store.memory_path.write_text("memory", encoding="utf-8")
    -    event = cast(
    -        PromptRenderCtx,
    -        SimpleNamespace(
    -            disabled_sections={"self_model", "long_term_memory"},
    -            system_sections_bottom=[],
    -        ),
    -    )
    -
    -    await _inject_profiles(event, store)
    -
    -    assert event.system_sections_bottom == []
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("consumer", "disabled", "visible"),
    -    [
    -        ("scheduler-subagent", {RETRIEVED_MEMORY_SECTION}, [SELF_PROFILE_SECTION, LONG_TERM_PROFILE_SECTION]),
    -        ("wake-evidence", {RETRIEVED_MEMORY_SECTION, LONG_TERM_PROFILE_SECTION}, [SELF_PROFILE_SECTION]),
    -        ("normal-wake-screen", set(), [SELF_PROFILE_SECTION, LONG_TERM_PROFILE_SECTION]),
    -    ],
    -)
    -async def test_consumer_scopes_project_exact_markdown_profile_visibility(
    -    tmp_path: Path,
    -    consumer: str,
    -    disabled: set[str],
    -    visible: list[str],
    -) -> None:
    -    store = _store(tmp_path / consumer)
    -    store.memory_path.write_text("# 用户长期记忆\n\n- 可见事实\n", encoding="utf-8")
    -    event = cast(
    -        PromptRenderCtx,
    -        SimpleNamespace(disabled_sections=disabled, system_sections_bottom=[]),
    -    )
    -
    -    await _inject_profiles(event, store)
    -
    -    assert [section.name for section in event.system_sections_bottom] == visible
    -
    -
    -def test_profile_draft_is_idempotent_and_recoverable(tmp_path: Path) -> None:
    -    store = _store(tmp_path)
    -    memory = "# 用户长期记忆\n\n## 用户事实\n- x\n\n## 用户偏好\n- y\n\n## 用户明确要求长期记住的关键内容\n- z\n"
    -    draft: dict[str, object] = {
    -        "version": 1,
    -        "memory": memory,
    -        "self": DEFAULT_SELF_MD,
    -        "memory_before": "",
    -        "self_before": DEFAULT_SELF_MD,
    -        "memory_before_digest": content_digest(""),
    -        "self_before_digest": content_digest(DEFAULT_SELF_MD),
    -        "memory_after_digest": content_digest(memory),
    -        "self_after_digest": content_digest(DEFAULT_SELF_MD),
    -    }
    -
    -    assert store.write_draft(
    -        "source:1", draft, session_key="session", generation=1
    -    ) == draft
    -    store.apply_draft("source:1", draft)
    -    store.apply_draft("source:1", draft)
    -
    -    assert store.read_memory() == memory
    -    assert store.read_self() == DEFAULT_SELF_MD
    -    assert store.is_applied("source:1")
    -    assert store.read_backup("source:1", "memory") == ""
    -    assert store.read_backup("source:1", "self") == DEFAULT_SELF_MD
    -
    -
    -def test_profile_receipt_conflict_fails_loud(tmp_path: Path) -> None:
    -    store = _store(tmp_path)
    -    first = {
    -        "version": 1,
    -        "memory": "",
    -        "self": DEFAULT_SELF_MD,
    -        "memory_before": "",
    -        "self_before": DEFAULT_SELF_MD,
    -    }
    -    _ = store.write_draft("source:1", first, session_key="session", generation=1)
    -
    -    with pytest.raises(ValueError, match="内容冲突"):
    -        _ = store.write_draft(
    -            "source:1",
    -            {**first, "version": 2},
    -            session_key="session",
    -            generation=1,
    -        )
    -
    -
    -def test_independent_document_receipts_recover_half_applied_draft(
    -    tmp_path: Path,
    -) -> None:
    -    store = _store(tmp_path)
    -    memory = "# 用户长期记忆\n\n## 用户事实\n- x\n\n## 用户偏好\n\n## 用户明确要求长期记住的关键内容\n"
    -    draft: dict[str, object] = {
    -        "version": 1,
    -        "memory": memory,
    -        "self": DEFAULT_SELF_MD,
    -        "memory_before": "",
    -        "self_before": DEFAULT_SELF_MD,
    -    }
    -    _ = store.write_draft(
    -        "source:crash", draft, session_key="session", generation=1
    -    )
    -    store._apply_document("source:crash", "memory", store.memory_path)
    -    assert not store.is_applied("source:crash")
    -
    -    reopened = _store(tmp_path)
    -    assert reopened.pending_source_refs() == ("source:crash",)
    -    reopened.apply_pending("source:crash")
    -
    -    assert reopened.is_applied("source:crash")
    -    assert reopened.read_memory() == memory
    -    assert reopened.read_self() == DEFAULT_SELF_MD
    -
    -
    -def test_pending_drafts_follow_durable_generation_not_lexical_source_ref(
    -    tmp_path: Path,
    -) -> None:
    -    store = _store(tmp_path)
    -    for generation in (10, 2, 1):
    -        draft = {
    -            "version": 1,
    -            "memory": "",
    -            "self": DEFAULT_SELF_MD,
    -            "memory_before": "",
    -            "self_before": DEFAULT_SELF_MD,
    -        }
    -        _ = store.write_draft(
    -            f"session:{generation}",
    -            draft,
    -            session_key="session",
    -            generation=generation,
    -        )
    -
    -    assert store.pending_source_refs() == ("session:1", "session:2", "session:10")
    -
    -
    -def test_profile_projection_rejects_implicit_fact_deletion() -> None:
    -    with pytest.raises(ValueError, match="不得隐式删除"):
    -        _validate_preserved_bullets(
    -            "# x\n- protected\n",
    -            "# x\n- replacement\n",
    -            document="MEMORY.md",
    -        )
    -
    -
    -def test_v4_source_plan_is_consumable() -> None:
    -    current = {
    -        "version": 4,
    -        "checkpoint": {
    -            "selected_source_messages": [
    -                {"id": "m1", "seq": 1, "message": {"role": "user", "content": "x"}}
    -            ]
    -        },
    -    }
    -
    -    assert '"id": "m1"' in _source_text(current)
    -
    -
    -def test_profile_validators_keep_memory_and_self_contracts() -> None:
    -    _validate_memory("")
    -    _validate_memory(
    -        "# 用户长期记忆\n\n## 用户事实\n- x\n\n## 用户偏好\n- y\n\n"
    -        "## 用户明确要求长期记住的关键内容\n- z\n"
    -    )
    -    _validate_self(DEFAULT_SELF_MD)
    -    with pytest.raises(ValueError, match="MEMORY.md"):
    -        _validate_memory("# arbitrary\n- x")
    -    with pytest.raises(ValueError, match="SELF.md"):
    -        _validate_self(DEFAULT_SELF_MD + "\n## 关系演进记录\n- no\n")
    -
    -
    -def test_pending_files_are_not_created(tmp_path: Path) -> None:
    -    _ = _store(tmp_path)
    -
    -    assert not (tmp_path / "memory/PENDING.md").exists()
    -    assert not (tmp_path / "memory/PENDING.snapshot.md").exists()
    -
    -
    -@pytest.mark.asyncio
    -async def test_markdown_candidate_and_publish_keep_formal_files_isolated(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    memory_dir = workspace / "memory"
    -    memory_dir.mkdir(parents=True)
    -    formal_memory = "# 用户长期记忆\n\n## 用户事实\n- formal\n\n## 用户偏好\n- formal\n\n## 用户明确要求长期记住的关键内容\n- formal\n"
    -    (memory_dir / "MEMORY.md").write_text(formal_memory, encoding="utf-8")
    -    (memory_dir / "SELF.md").write_text(DEFAULT_SELF_MD, encoding="utf-8")
    -    (memory_dir / "SECRET.md").write_text("not granted", encoding="utf-8")
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(markdown_plugin.__file__).parent,
    -            Path(__file__).parent / "fixtures/static_chat_models",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -    generation = stable.generations["markdown_memory"]
    -    assert generation.instance.workspace_roots == ()
    -    assert generation.instance.workspace_files == markdown_plugin.workspace_files
    -    lease = await manager.snapshot_store.acquire()
    -    candidate = await manager.prepare_candidate("markdown_memory")
    -    assert candidate is not None and candidate.validation_workspace is not None
    -    assert not tuple(candidate.validation_workspace.rglob("SECRET.md"))
    -    candidate_snapshot = candidate.runtime_snapshot
    -    assert candidate_snapshot is not None
    -    candidate_root = candidate_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_runtime = candidate_root.plugin_runtime("markdown_memory")
    -    assert candidate_runtime.workspace_file("memory/MEMORY.md").read_text(
    -        encoding="utf-8"
    -    ) == formal_memory
    -
    -    publish_task = asyncio.create_task(manager.publish_prepared("markdown_memory"))
    -    await asyncio.sleep(0)
    -    assert manager.current_snapshot is stable
    -    assert lease.snapshot is stable
    -    await lease.release()
    -    published = await publish_task
    -
    -    assert published["publication_state"] == "committed"
    -    assert manager.current_snapshot is not stable
    -    assert (memory_dir / "MEMORY.md").read_text(encoding="utf-8") == formal_memory
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_markdown_builtin_can_be_disabled(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(markdown_plugin.__file__).parent,
    -            Path(__file__).parent / "fixtures/static_chat_models",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -        disabled_builtin_plugins=frozenset({"markdown_memory"}),
    -    )
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert "markdown_memory" not in snapshot.generations
    -    assert not (workspace / "memory/MEMORY.md").exists()
    -    await manager.terminate_all()
    -    sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_legacy_pending_is_archived_after_one_direct_merge(tmp_path: Path) -> None:
    -    store = _store(tmp_path)
    -    pending = tmp_path / "memory/PENDING.md"
    -    snapshot = tmp_path / "memory/PENDING.snapshot.md"
    -    retired = tmp_path / "memory/PENDING.retired.md"
    -    lock = tmp_path / "memory/markdown-profile.lock"
    -    pending.write_text("- [identity] pending\n", encoding="utf-8")
    -    snapshot.write_text("- [preference] snapshot\n", encoding="utf-8")
    -    await _migrate_pending(
    -        store,
    -        lock,
    -        pending,
    -        snapshot,
    -        retired,
    -    )
    -
    -    assert pending.read_text(encoding="utf-8") == ""
    -    assert snapshot.read_text(encoding="utf-8") == ""
    -    archive = retired.read_text(encoding="utf-8")
    -    assert '"pending": "- [identity] pending\\n"' in archive
    -    assert '"snapshot": "- [preference] snapshot\\n"' in archive
    -    memory = store.read_memory()
    -    assert "- [identity] pending" in memory
    -    assert "- [preference] snapshot" in memory
    -
    -    pending.write_text("- [identity] restored later\n", encoding="utf-8")
    -    with pytest.raises(RuntimeError, match="出现新内容"):
    -        await _migrate_pending(
    -            store,
    -            lock,
    -            pending,
    -            snapshot,
    -            retired,
    -        )
    -    assert pending.read_text(encoding="utf-8") == "- [identity] restored later\n"
    -
    -
    -@pytest.mark.asyncio
    -async def test_v2_draft_rejects_inner_source_ref_mismatch(tmp_path: Path) -> None:
    -    receipt = {
    -        "version": 2,
    -        "markdown_draft": {
    -            "source_ref": "wrong",
    -            "pending_items": "",
    -            "history_entry_payloads": [],
    -            "conversation": "",
    -            "scope_channel": "",
    -            "scope_chat_id": "",
    -        },
    -    }
    -    with pytest.raises(ValueError, match="source_ref 冲突"):
    -        await _prepare_draft(
    -            json.dumps(receipt),
    -            "expected",
    -            _store(tmp_path),
    -            cast(Any, None),
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_runtime_started_migrates_legacy_pending_once(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    pending = workspace / "memory/PENDING.md"
    -    pending.parent.mkdir(parents=True)
    -    pending.write_text("- [requested_memory] keep exact\n", encoding="utf-8")
    -    sessions = SessionManager(workspace)
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            Path(markdown_plugin.__file__).parent,
    -            Path(__file__).parent / "fixtures/static_chat_models",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    retired = workspace / "memory/PENDING.retired.md"
    -    try:
    -        await cast(Any, manager)._start_current_runtime_snapshot()
    -        assert retired.exists()
    -        assert pending.read_text(encoding="utf-8") == ""
    -        assert "- [requested_memory] keep exact" in (
    -            workspace / "memory/MEMORY.md"
    -        ).read_text(encoding="utf-8")
    -    finally:
    -        await manager.terminate_all()
    -        sessions.close()
    diff --git a/tests/test_memory_plugin_claim.py b/tests/test_memory_plugin_claim.py
    deleted file mode 100644
    index 268caf4e6..000000000
    --- a/tests/test_memory_plugin_claim.py
    +++ /dev/null
    @@ -1,827 +0,0 @@
    -from __future__ import annotations
    -
    -from datetime import UTC, datetime
    -from pathlib import Path
    -from types import SimpleNamespace
    -from unittest.mock import AsyncMock
    -import asyncio
    -import json
    -import os
    -import shutil
    -import subprocess
    -import sys
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    AddModel,
    -    CapabilitySources,
    -    CompositionRoot,
    -    CONVERSATION_SEMANTIC_INTEREST,
    -    COMMANDS,
    -    EMBEDDING_MEMORY_PLUGIN,
    -    EMBEDDINGS,
    -    EmbeddingSpaceDescriptor,
    -    INTERACTION_UNDO,
    -    SNAPSHOT_SEALING,
    -    RUNTIME_STOPPING,
    -    RUNTIME_STARTED,
    -    TOOL_CATALOG,
    -    UI_SLOTS,
    -    ModelCapabilities,
    -    ModelKind,
    -    SetDefaultModel,
    -    PluginRuntime,
    -    PluginCommands,
    -    PluginTools,
    -    PluginUiSlots,
    -    SnapshotSealing,
    -    RuntimeStopping,
    -    RuntimeStarted,
    -)
    -from agent.plugin_composition.interaction_undo import InteractionUndoService
    -from agent.plugin_composition.diagnostics import CorePluginDiagnostics
    -from agent.lifecycle.types import PromptRenderCtx
    -from agent.lifecycle.composition import PROMPT_RENDER_EVENT
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.install import (
    -    finalize_uninstall_plugin,
    -    install_git_plugin,
    -    set_installed_plugin_enabled,
    -)
    -from agent.plugins.snapshot import (
    -    RuntimeSnapshotCompiler,
    -    RuntimeSnapshotStore,
    -    bind_runtime_snapshot,
    -    get_current_runtime_snapshot,
    -    reset_runtime_snapshot,
    -)
    -from agent.tools.registry import ToolRegistry
    -from agent.turn_events.after_turn import AFTER_TURN_COMMITTED
    -from bus.event_bus import EventBus
    -from bus.events_lifecycle import TurnCommitted
    -from core.memory.engine import MemoryQueryResult
    -from plugins.akasha.engine import AkashaMemoryEngine
    -from plugins.akasha.plugin import _AkashaRuntimeHandle, _inject_memory
    -from plugins.akasha import plugin as akasha_plugin
    -from plugins.models.store import ModelsStore
    -from session.manager import SessionManager
    -
    -
    -class _QueryRuntimeStub:
    -    def __init__(self, result: MemoryQueryResult | None = None) -> None:
    -        self.query = AsyncMock(return_value=result)
    -
    -
    -class _RepositoryAkashaImportBlocker:
    -    def find_spec(
    -        self,
    -        fullname: str,
    -        path: object = None,
    -        target: object = None,
    -    ) -> None:
    -        _ = path, target
    -        if fullname == "plugins.akasha" or fullname.startswith("plugins.akasha."):
    -            raise ModuleNotFoundError(f"repository plugin import blocked: {fullname}")
    -        return None
    -
    -
    -def _commit_plugin(repo: Path) -> None:
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "init"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -
    -
    -def _diagnostics() -> CorePluginDiagnostics:
    -    return CorePluginDiagnostics(
    -        plugin_id="akasha",
    -        generation_id="test-generation",
    -        fiber="memory-claim-test",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_memory_plugin_claim_is_declarative_and_exclusive() -> None:
    -    root = CompositionRoot("memory-claim")
    -
    -    async def first_memory(ctx) -> None:
    -        _ = await ctx.provide(EMBEDDING_MEMORY_PLUGIN, object())
    -
    -    await root.mount(first_memory, name="first-memory")
    -
    -    async def second_memory(ctx) -> None:
    -        _ = await ctx.provide(EMBEDDING_MEMORY_PLUGIN, object())
    -
    -    await root.mount(second_memory, name="akasha")
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is False
    -    assert receipt.required_pending == ("akasha",)
    -    assert any(
    -        incident.owner == "akasha"
    -        and "DUPLICATE_SERVICE" in incident.message
    -        and "plugin.claim.embedding_memory" in incident.message
    -        for incident in receipt.incidents
    -    )
    -    await root.dispose()
    -
    -
    -def _manager(
    -    tmp_path: Path,
    -    *plugin_names: str,
    -) -> tuple[PluginManager, SessionManager]:
    -    plugin_root = Path(__file__).resolve().parents[1] / "plugins"
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    return (
    -        PluginManager(
    -            [plugin_root / name for name in plugin_names],
    -            event_bus=EventBus(),
    -            tool_registry=ToolRegistry(),
    -            workspace=workspace,
    -            session_manager=sessions,
    -            installed_cache_root=tmp_path / "plugin-home" / "cache",
    -        ),
    -        sessions,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_starts_as_an_ordinary_memory_provider(tmp_path: Path) -> None:
    -    manager, sessions = _manager(tmp_path, "akasha", "models", "openai_compatible")
    -    workspace = tmp_path / "workspace"
    -    store = ModelsStore(
    -        workspace / "model-registry.sqlite3",
    -        backup_dir=workspace / "runtime" / "model-backups",
    -        writable=True,
    -    )
    -    revision = store.add_connection(
    -        AddConnection(
    -            expected_revision=0,
    -            connection_id="fixture-connection",
    -            name="Fixture",
    -            driver_id="openai-compatible",
    -            endpoint="http://127.0.0.1:9/v1",
    -            auth_identity="fixture-account",
    -            credential={"driver": "api_key", "access_token": "fixture"},
    -        )
    -    )
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="fixture-embedding",
    -            connection_id="fixture-connection",
    -            kind=ModelKind.EMBEDDING,
    -            model="fixture-embedding",
    -            capabilities=ModelCapabilities(
    -                embedding_dimensions=32,
    -                embedding_normalization="none",
    -            ),
    -            capability_sources=CapabilitySources(
    -                embedding_dimensions="fixture",
    -                embedding_normalization="fixture",
    -            ),
    -        )
    -    )
    -    _ = store.set_default(
    -        SetDefaultModel(
    -            expected_revision=revision,
    -            role=None,
    -            model_id="fixture-embedding",
    -        )
    -    )
    -    try:
    -        await manager.load_all()
    -        assert {item.plugin_id for item in manager.active_plugins()} == {
    -            "akasha",
    -            "models",
    -            "openai-compatible",
    -        }
    -        assert manager.current_snapshot is not None
    -        topology = manager.current_snapshot.composition_topology
    -        assert topology is not None
    -        assert "plugin.claim.embedding_memory" in topology.services
    -    finally:
    -        await manager.terminate_all()
    -        sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_installs_without_repository_package(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    """Akasha remains a normal installable artifact when its source tree is absent."""
    -
    -    repo = tmp_path / "akasha-repo"
    -    shutil.copytree(Path("plugins/akasha"), repo)
    -    shutil.rmtree(repo / "__pycache__", ignore_errors=True)
    -    _commit_plugin(repo)
    -    workspace = tmp_path / "workspace"
    -    installed = install_git_plugin(
    -        workspace=workspace,
    -        source=str(repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "plugin-home",
    -    )
    -    for module_name in tuple(sys.modules):
    -        if module_name == "plugins.akasha" or module_name.startswith(
    -            "plugins.akasha."
    -        ):
    -            monkeypatch.delitem(sys.modules, module_name)
    -    monkeypatch.setattr(
    -        sys,
    -        "meta_path",
    -        [_RepositoryAkashaImportBlocker(), *sys.meta_path],
    -    )
    -    plugin_root = Path(__file__).resolve().parents[1] / "plugins"
    -
    -    def installed_manager() -> tuple[PluginManager, SessionManager]:
    -        sessions = SessionManager(workspace)
    -        return (
    -            PluginManager(
    -                [plugin_root / "models", plugin_root / "openai_compatible"],
    -                event_bus=EventBus(),
    -                workspace=workspace,
    -                session_manager=sessions,
    -                installed_cache_root=tmp_path / "plugin-home" / "cache",
    -            ),
    -            sessions,
    -        )
    -
    -    manager, sessions = installed_manager()
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("akasha@ordinary-test")
    -        assert generation is not None
    -        assert generation.plugin_dir == installed.installed_path
    -        assert generation.source_type == "installed"
    -        package = generation.instance.module.__package__
    -        assert package
    -        installed_modules = [
    -            module
    -            for module_name, module in sys.modules.items()
    -            if module_name == package or module_name.startswith(f"{package}.")
    -        ]
    -        assert installed_modules
    -        for module in installed_modules:
    -            module_file = module.__file__
    -            if module_file is not None:
    -                assert Path(module_file).resolve().is_relative_to(
    -                    installed.installed_path
    -                )
    -    finally:
    -        await manager.terminate_all()
    -        sessions.close()
    -
    -    set_installed_plugin_enabled(
    -        "akasha@ordinary-test",
    -        enabled=False,
    -        plugins_home=tmp_path / "plugin-home",
    -    )
    -    _ = finalize_uninstall_plugin(
    -        "akasha@ordinary-test",
    -        workspace=workspace,
    -        plugins_home=tmp_path / "plugin-home",
    -    )
    -    without, sessions = installed_manager()
    -    try:
    -        await without.load_all()
    -        assert without.generation("akasha@ordinary-test") is None
    -    finally:
    -        await without.terminate_all()
    -        sessions.close()
    -
    -    _ = install_git_plugin(
    -        workspace=workspace,
    -        source=str(repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "plugin-home",
    -    )
    -    restored, sessions = installed_manager()
    -    try:
    -        await restored.load_all()
    -        assert restored.generation("akasha@ordinary-test") is not None
    -    finally:
    -        await restored.terminate_all()
    -        sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_without_embedding_degrades_prompt_and_wake_scoring(
    -    tmp_path: Path,
    -) -> None:
    -    """An optional memory lane must not break a normal Turn or Wake maintenance."""
    -
    -    manager, sessions = _manager(tmp_path, "akasha", "models", "openai_compatible")
    -    try:
    -        await manager.load_all()
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        root = snapshot.composition_root
    -        embedding_health = next(
    -            item for item in root.receipt().health if item.name == "embedding"
    -        )
    -        assert embedding_health.required is False
    -        assert embedding_health.healthy is False
    -        assert embedding_health.reason
    -
    -        prompt = PromptRenderCtx(
    -            session_key="web:one",
    -            channel="web",
    -            chat_id="one",
    -            content="hello",
    -            media=None,
    -            timestamp=datetime(2026, 8, 29, tzinfo=UTC),
    -            history=[],
    -            skill_names=[],
    -            disabled_sections=set(),
    -            turn_injection_prompt="",
    -        )
    -        lease = await manager._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            await root.context.serial(PROMPT_RENDER_EVENT, prompt)
    -            assert snapshot.tool_registry is not None
    -            snapshot.tool_registry.set_context(turn_id="turn:memory-unavailable")
    -            result = await snapshot.tool_registry.execute(
    -                "recall_memory",
    -                {"query": "hello"},
    -                raise_errors=True,
    -            )
    -            feedback_result = await snapshot.tool_registry.execute(
    -                "remember_memory",
    -                {
    -                    "message_ids": ["current_user_message"],
    -                    "reason": "remember this correction",
    -                },
    -                raise_errors=True,
    -            )
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -        assert prompt.system_sections_bottom == []
    -        assert isinstance(result, str)
    -        payload = json.loads(result)
    -        assert payload["count"] == 0
    -        assert payload["items"] == []
    -        assert payload["error"] == "memory_unavailable"
    -        assert payload["reason"] == embedding_health.reason
    -        assert isinstance(feedback_result, str)
    -        assert json.loads(feedback_result) == {
    -            "status": "not_staged",
    -            "error": "memory_unavailable",
    -            "reason": embedding_health.reason,
    -        }
    -
    -        semantic = root.context.require(CONVERSATION_SEMANTIC_INTEREST)
    -        scores = await semantic.score(
    -            ("due content",),
    -            cutoff="2026-08-29T00:00:00+00:00",
    -        )
    -        assert scores == (0.0,)
    -    finally:
    -        await manager.terminate_all()
    -        sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_stops_using_an_old_embedding_after_default_changes(
    -    tmp_path: Path,
    -) -> None:
    -    """A live kernel must not keep writing its old space after settings change."""
    -
    -    selected = {"identity": "space-a"}
    -    projected: list[str] = []
    -
    -    class Runtime(AkashaMemoryEngine):
    -        closeables: tuple[object, ...] = ()
    -
    -        def __init__(self) -> None:
    -            pass
    -
    -        @property
    -        def embedding_api(self):
    -            return type("EmbeddingApi", (), {"model_id": "space-a"})()
    -
    -        async def project_committed_turn(self, event: TurnCommitted) -> None:
    -            projected.append(event.turn_id)
    -
    -    handle = _AkashaRuntimeHandle()
    -    handle.configure(
    -        Runtime,
    -        embedding_identity=lambda: selected["identity"],
    -    )
    -    assert handle.available() is True
    -    selected["identity"] = "space-b"
    -    assert handle.available() is False
    -    assert handle.model_id == ""
    -    await handle.project_committed_turn(
    -        TurnCommitted(
    -            session_key="test:one",
    -            channel="test",
    -            chat_id="one",
    -            input_message="hello",
    -            persisted_user_message="hello",
    -            assistant_response="world",
    -            tools_used=[],
    -            turn_id="turn:changed",
    -        )
    -    )
    -    assert projected == []
    -
    -    prompt = PromptRenderCtx(
    -        session_key="web:one",
    -        channel="web",
    -        chat_id="one",
    -        content="hello",
    -        media=None,
    -        timestamp=datetime(2026, 8, 29, tzinfo=UTC),
    -        history=[],
    -        skill_names=[],
    -        disabled_sections=set(),
    -        turn_injection_prompt="",
    -    )
    -    await _inject_memory(prompt, handle, _diagnostics())
    -    assert prompt.system_sections_bottom == []
    -
    -    selected["identity"] = "space-a"
    -    assert handle.available() is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_post_commit_worker_uses_the_source_snapshot(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    """The detached projector must bind the exact lease captured by its event."""
    -
    -    projected = asyncio.Event()
    -
    -    class Embeddings:
    -        def describe(self, *, model_id: str | None = None):
    -            _ = model_id
    -            return type("Descriptor", (), {"identity": "test-space"})()
    -
    -        async def assert_bound(self) -> None:
    -            assert get_current_runtime_snapshot() is not None
    -
    -    embeddings = Embeddings()
    -
    -    class Runtime:
    -        closeables: tuple[object, ...] = ()
    -        embedding_api = type("EmbeddingApi", (), {"model_id": "test-space"})()
    -
    -        async def project_committed_turn(self, event: TurnCommitted) -> None:
    -            assert event.turn_id == "turn:queued"
    -            await embeddings.assert_bound()
    -            projected.set()
    -
    -    runtime = Runtime()
    -    monkeypatch.setattr(akasha_plugin, "_build_runtime", lambda **_: runtime)
    -
    -    async def skip_tools(*_args: object) -> None:
    -        return None
    -
    -    monkeypatch.setattr(akasha_plugin, "_register_tools", skip_tools)
    -    root = CompositionRoot("akasha-post-commit-scope")
    -    store = RuntimeSnapshotStore()
    -    root._bind_runtime_scope_acquirer(
    -        lambda: store.acquire_composition_root(root)
    -    )
    -    _ = await root.context.provide(EMBEDDINGS, embeddings)
    -    _ = await root.context.provide(COMMANDS, PluginCommands())
    -    _ = await root.context.provide(TOOL_CATALOG, PluginTools(root.instance_token))
    -    _ = await root.context.provide(UI_SLOTS, PluginUiSlots())
    -    _ = await root.context.provide(
    -        INTERACTION_UNDO,
    -        InteractionUndoService.candidate_validation(),
    -    )
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "sessions.db").touch()
    -    _ = await root.mount(
    -        lambda ctx: akasha_plugin.apply(ctx, None),
    -        name="akasha",
    -        inject=(COMMANDS, EMBEDDINGS, INTERACTION_UNDO, TOOL_CATALOG, UI_SLOTS),
    -        runtime=PluginRuntime(
    -            plugin_id="akasha",
    -            generation_id="akasha:test",
    -            plugin_dir=Path("plugins/akasha").resolve(),
    -            data_dir=tmp_path / "plugin-data",
    -            workspace=workspace,
    -            config=None,
    -            workspace_roots=("memory",),
    -            workspace_files=("sessions.db",),
    -        ),
    -    )
    -    assert root.receipt().ready, [item.message for item in root.receipt().incidents]
    -    snapshot = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    store.install(snapshot)
    -    lease = store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    try:
    -        await root.context.serial(SNAPSHOT_SEALING, SnapshotSealing())
    -        root.context.emit(
    -            AFTER_TURN_COMMITTED,
    -            TurnCommitted(
    -                session_key="test:one",
    -                channel="test",
    -                chat_id="one",
    -                input_message="hello",
    -                persisted_user_message="hello",
    -                assistant_response="world",
    -                tools_used=[],
    -                turn_id="turn:queued",
    -            ),
    -        )
    -        await asyncio.wait_for(projected.wait(), timeout=1)
    -        await asyncio.sleep(0)
    -        assert snapshot.lease_count == 1
    -        await root.context.serial(RUNTIME_STOPPING, RuntimeStopping())
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -        await root.dispose()
    -        await store.close()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("repair_fails", [False, True])
    -async def test_akasha_reindex_worker_runs_after_public_start_and_retains_failure(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    repair_fails: bool,
    -) -> None:
    -    """Hot-start repair uses the public Root and clears intent only on success."""
    -
    -    attempted = asyncio.Event()
    -    finished: list[Path] = []
    -
    -    class Embeddings:
    -        def describe(self, *, model_id: str | None = None):
    -            _ = model_id
    -            return SimpleNamespace(identity="test-space", model_id="embedding")
    -
    -    class Runtime:
    -        closeables: tuple[object, ...] = ()
    -        embedding_api = SimpleNamespace(model_id="test-space")
    -
    -    async def fake_reindex(**kwargs: object):
    -        runtime_scope = kwargs["runtime_scope"]
    -        async with runtime_scope():  # type: ignore[operator]
    -            assert get_current_runtime_snapshot() is not None
    -        attempted.set()
    -        if repair_fails:
    -            raise RuntimeError("injected repair failure")
    -        return SimpleNamespace(embedded_messages=2)
    -
    -    monkeypatch.setattr(akasha_plugin, "_build_runtime", lambda **_: Runtime())
    -    monkeypatch.setattr(akasha_plugin, "_register_tools", AsyncMock())
    -    monkeypatch.setattr(akasha_plugin, "load_request", lambda _root: object())
    -    monkeypatch.setattr(akasha_plugin, "reindex", fake_reindex)
    -    monkeypatch.setattr(
    -        akasha_plugin,
    -        "finish_request",
    -        lambda root: finished.append(root),
    -    )
    -
    -    root = CompositionRoot("akasha-reindex-hot-start")
    -    store = RuntimeSnapshotStore()
    -    root._bind_runtime_scope_acquirer(lambda: store.acquire_composition_root(root))
    -    _ = await root.context.provide(EMBEDDINGS, Embeddings())
    -    _ = await root.context.provide(COMMANDS, PluginCommands())
    -    _ = await root.context.provide(TOOL_CATALOG, PluginTools(root.instance_token))
    -    _ = await root.context.provide(UI_SLOTS, PluginUiSlots())
    -    _ = await root.context.provide(
    -        INTERACTION_UNDO,
    -        InteractionUndoService.candidate_validation(),
    -    )
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "sessions.db").touch()
    -    _ = await root.mount(
    -        lambda ctx: akasha_plugin.apply(ctx, None),
    -        name="akasha",
    -        inject=(COMMANDS, EMBEDDINGS, INTERACTION_UNDO, TOOL_CATALOG, UI_SLOTS),
    -        runtime=PluginRuntime(
    -            plugin_id="akasha",
    -            generation_id="akasha:repair",
    -            plugin_dir=Path("plugins/akasha").resolve(),
    -            data_dir=tmp_path / "plugin-data",
    -            workspace=workspace,
    -            config=None,
    -            workspace_roots=("memory",),
    -            workspace_files=("sessions.db",),
    -        ),
    -    )
    -    await root.context.serial(SNAPSHOT_SEALING, SnapshotSealing())
    -    snapshot = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    store.install(snapshot)
    -    await root.context.serial(RUNTIME_STARTED, RuntimeStarted())
    -    await asyncio.wait_for(attempted.wait(), timeout=1)
    -    await asyncio.sleep(0)
    -
    -    assert finished == ([] if repair_fails else [tmp_path / "plugin-data"])
    -    if repair_fails:
    -        assert any(
    -            incident.kind == "akasha.reindex_failed"
    -            for incident in root.receipt().incidents
    -        )
    -    await root.context.serial(RUNTIME_STOPPING, RuntimeStopping())
    -    await root.dispose()
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_reindex_cancel_retains_request_for_fresh_root_retry(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    """Root retirement keeps repair intent and releases its exact scope."""
    -
    -    descriptor = EmbeddingSpaceDescriptor(
    -        plugin_snapshot_id="snapshot",
    -        model_revision=1,
    -        model_id="embedding",
    -        connection_id="connection",
    -        driver_id="driver",
    -        driver_contract_version="1",
    -        auth_identity="account",
    -        connection_fingerprint="endpoint",
    -        model="embedding",
    -        dimensions=3,
    -        normalization="none",
    -        capability_digest="caps",
    -    )
    -    entered = asyncio.Event()
    -    block_first = asyncio.Event()
    -    request_finished = asyncio.Event()
    -    calls = 0
    -    bound_snapshots: list[object] = []
    -    data_root = tmp_path / "plugin-data"
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "sessions.db").touch()
    -    _ = akasha_plugin.save_request(data_root, descriptor)
    -
    -    class Embeddings:
    -        def describe(self, *, model_id: str | None = None):
    -            _ = model_id
    -            return descriptor
    -
    -    class Runtime:
    -        closeables: tuple[object, ...] = ()
    -        embedding_api = SimpleNamespace(model_id=descriptor.identity)
    -
    -    async def fake_reindex(**kwargs: object):
    -        nonlocal calls
    -        calls += 1
    -        runtime_scope = kwargs["runtime_scope"]
    -        async with runtime_scope():  # type: ignore[operator]
    -            snapshot = get_current_runtime_snapshot()
    -            assert snapshot is not None
    -            bound_snapshots.append(snapshot)
    -            if calls == 1:
    -                entered.set()
    -                await block_first.wait()
    -        return SimpleNamespace(embedded_messages=2)
    -
    -    real_finish_request = akasha_plugin.finish_request
    -
    -    def finish_request(root: Path) -> None:
    -        real_finish_request(root)
    -        request_finished.set()
    -
    -    monkeypatch.setattr(akasha_plugin, "_build_runtime", lambda **_: Runtime())
    -    monkeypatch.setattr(akasha_plugin, "_register_tools", AsyncMock())
    -    monkeypatch.setattr(akasha_plugin, "reindex", fake_reindex)
    -    monkeypatch.setattr(akasha_plugin, "finish_request", finish_request)
    -
    -    async def mount_root(generation: str):
    -        root = CompositionRoot(generation)
    -        store = RuntimeSnapshotStore()
    -        root._bind_runtime_scope_acquirer(
    -            lambda: store.acquire_composition_root(root)
    -        )
    -        _ = await root.context.provide(EMBEDDINGS, Embeddings())
    -        _ = await root.context.provide(COMMANDS, PluginCommands())
    -        _ = await root.context.provide(
    -            TOOL_CATALOG,
    -            PluginTools(root.instance_token),
    -        )
    -        _ = await root.context.provide(UI_SLOTS, PluginUiSlots())
    -        _ = await root.context.provide(
    -            INTERACTION_UNDO,
    -            InteractionUndoService.candidate_validation(),
    -        )
    -        _ = await root.mount(
    -            lambda ctx: akasha_plugin.apply(ctx, None),
    -            name="akasha",
    -            inject=(
    -                COMMANDS,
    -                EMBEDDINGS,
    -                INTERACTION_UNDO,
    -                TOOL_CATALOG,
    -                UI_SLOTS,
    -            ),
    -            runtime=PluginRuntime(
    -                plugin_id="akasha",
    -                generation_id=generation,
    -                plugin_dir=Path("plugins/akasha").resolve(),
    -                data_dir=data_root,
    -                workspace=workspace,
    -                config=None,
    -                workspace_roots=("memory",),
    -                workspace_files=("sessions.db",),
    -            ),
    -        )
    -        await root.context.serial(SNAPSHOT_SEALING, SnapshotSealing())
    -        snapshot = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -        store.install(snapshot)
    -        await root.context.serial(RUNTIME_STARTED, RuntimeStarted())
    -        return root, store, snapshot
    -
    -    first_root, first_store, first_snapshot = await mount_root("akasha:first")
    -    await asyncio.wait_for(entered.wait(), timeout=1)
    -    assert akasha_plugin.load_request(data_root) is not None
    -    assert first_snapshot.lease_count == 1
    -
    -    await first_root.dispose()
    -    assert first_snapshot.lease_count == 0
    -    assert akasha_plugin.load_request(data_root) is not None
    -    assert not request_finished.is_set()
    -    await first_store.close()
    -
    -    second_root, second_store, second_snapshot = await mount_root("akasha:second")
    -    await asyncio.wait_for(request_finished.wait(), timeout=1)
    -    assert akasha_plugin.load_request(data_root) is None
    -    assert calls == 2
    -    assert bound_snapshots == [first_snapshot, second_snapshot]
    -    await second_root.context.serial(RUNTIME_STOPPING, RuntimeStopping())
    -    await second_root.dispose()
    -    await second_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_injects_recall_as_an_ordinary_prompt_section() -> None:
    -    runtime = _QueryRuntimeStub(
    -        MemoryQueryResult(
    -            text_block="embedded recall",
    -            records=[],
    -            raw={},
    -        )
    -    )
    -    event = PromptRenderCtx(
    -        session_key="web:one",
    -        channel="web",
    -        chat_id="one",
    -        content="hello",
    -        media=None,
    -        timestamp=datetime(2026, 8, 25, tzinfo=UTC),
    -        history=[],
    -        skill_names=[],
    -        disabled_sections=set(),
    -        turn_injection_prompt="",
    -    )
    -
    -    await _inject_memory(event, runtime, _diagnostics())
    -
    -    assert [(item.name, item.content) for item in event.system_sections_bottom] == [
    -        ("memory", "embedded recall")
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_akasha_prompt_section_obeys_generic_disable_switch() -> None:
    -    runtime = _QueryRuntimeStub()
    -    event = PromptRenderCtx(
    -        session_key="scheduler:one",
    -        channel="scheduler",
    -        chat_id="one",
    -        content="tick",
    -        media=None,
    -        timestamp=datetime(2026, 8, 25, tzinfo=UTC),
    -        history=[],
    -        skill_names=[],
    -        disabled_sections={"memory"},
    -        turn_injection_prompt="",
    -    )
    -
    -    await _inject_memory(event, runtime, _diagnostics())
    -
    -    runtime.query.assert_not_awaited()
    -    assert event.system_sections_bottom == []
    diff --git a/tests/test_memory_window_alignment.py b/tests/test_memory_window_alignment.py
    deleted file mode 100644
    index 02b149006..000000000
    --- a/tests/test_memory_window_alignment.py
    +++ /dev/null
    @@ -1,8 +0,0 @@
    -from agent.looping.ports import AgentLoopConfig
    -
    -
    -def test_agent_loop_config_does_not_own_plugin_memory_policy() -> None:
    -    config = AgentLoopConfig()
    -
    -    assert not hasattr(config, "context_compaction")
    -    assert not hasattr(config, "memory")
    diff --git a/tests/test_message_lookup_tool.py b/tests/test_message_lookup_tool.py
    deleted file mode 100644
    index 7745ebbc9..000000000
    --- a/tests/test_message_lookup_tool.py
    +++ /dev/null
    @@ -1,426 +0,0 @@
    -import json
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.tools.message_lookup import FetchMessagesTool, SearchMessagesTool
    -from prompts.agent import build_agent_behavior_rules_prompt
    -from session.manager import SessionManager
    -from session.store import SessionStore
    -
    -
    -def _setup_session(store: SessionStore, key: str, n_messages: int) -> None:
    -    store.upsert_session(
    -        key,
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    roles = ["user", "assistant"]
    -    for seq in range(n_messages):
    -        store.insert_message(
    -            key,
    -            role=roles[seq % 2],
    -            content=f"msg-{seq}",
    -            ts=f"2026-01-01T00:00:{seq:02d}+00:00",
    -            seq=seq,
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_returns_rows_in_input_order(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 2)
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(await tool.execute(ids=["tg:1:1", "tg:1:0"]))
    -
    -    assert payload["count"] == 2
    -    assert payload["matched_count"] == 2
    -    assert [m["id"] for m in payload["messages"]] == ["tg:1:1", "tg:1:0"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_strips_internal_metadata(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.upsert_session(
    -        "tg:1",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    store.insert_message(
    -        "tg:1",
    -        role="assistant",
    -        content="answer",
    -        ts="2026-01-01T00:00:00+00:00",
    -        seq=0,
    -        tool_chain=[
    -            {
    -                "calls": [
    -                    {
    -                        "name": "fetch_messages",
    -                        "arguments": {},
    -                        "result": "huge",
    -                    }
    -                ]
    -            }
    -        ],
    -        extra={"tools_used": ["fetch_messages"], "reasoning_content": "think"},
    -    )
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(await tool.execute(ids=["tg:1:0"]))
    -
    -    assert payload["messages"] == [
    -        {
    -            "id": "tg:1:0",
    -            "session_key": "tg:1",
    -            "seq": 0,
    -            "role": "assistant",
    -            "content": "answer",
    -            "timestamp": "2026-01-01T00:00:00+00:00",
    -        }
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_with_context(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 7)  # seq 0..6
    -
    -    tool = FetchMessagesTool(store)
    -    # fetch seq=3, context=2 → expect seq 1..5
    -    payload = json.loads(await tool.execute(ids=["tg:1:3"], context=2))
    -
    -    ids = [m["id"] for m in payload["messages"]]
    -    assert "tg:1:3" in ids
    -    assert "tg:1:1" in ids
    -    assert "tg:1:5" in ids
    -    assert "tg:1:0" not in ids
    -    assert "tg:1:6" not in ids
    -    assert payload["matched_count"] == 1
    -    assert payload["count"] == 5
    -
    -    # in_source_ref flag: only the hit is True
    -    hit = next(m for m in payload["messages"] if m["id"] == "tg:1:3")
    -    ctx_msg = next(m for m in payload["messages"] if m["id"] == "tg:1:1")
    -    assert hit["in_source_ref"] is True
    -    assert ctx_msg["in_source_ref"] is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_context_clamps_at_seq_zero(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 3)  # seq 0,1,2
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(await tool.execute(ids=["tg:1:0"], context=3))
    -
    -    # context before seq 0 is clamped; should get seq 0,1,2,3 — but only 0-2 exist
    -    ids = [m["id"] for m in payload["messages"]]
    -    assert "tg:1:0" in ids
    -    assert "tg:1:1" in ids
    -    assert "tg:1:2" in ids
    -    assert payload["matched_count"] == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_context_clamps_at_max_window(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 30)  # seq 0..29
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(await tool.execute(ids=["tg:1:11"], context=999))
    -
    -    ids = [m["id"] for m in payload["messages"]]
    -    assert ids[0] == "tg:1:1"
    -    assert ids[-1] == "tg:1:21"
    -    assert "tg:1:0" not in ids
    -    assert "tg:1:22" not in ids
    -    assert payload["matched_count"] == 1
    -    assert payload["count"] == 21
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_supports_window_source_ref(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 6)
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(
    -        await tool.execute(source_ref='["tg:1:2","tg:1:3"]#profile', context=1)
    -    )
    -
    -    assert [m["id"] for m in payload["messages"]] == [
    -        "tg:1:1",
    -        "tg:1:2",
    -        "tg:1:3",
    -        "tg:1:4",
    -    ]
    -    assert payload["matched_count"] == 2
    -    assert [m["in_source_ref"] for m in payload["messages"]] == [False, True, True, False]
    -
    -
    -@pytest.mark.asyncio
    -async def test_fetch_messages_supports_mixed_ids_and_source_refs(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _setup_session(store, "tg:1", 5)
    -
    -    tool = FetchMessagesTool(store)
    -    payload = json.loads(
    -        await tool.execute(
    -            ids=["tg:1:4"],
    -            source_refs=['["tg:1:1","tg:1:2"]#h:abc', "tg:1:4"],
    -        )
    -    )
    -
    -    assert [m["id"] for m in payload["messages"]] == ["tg:1:4", "tg:1:1", "tg:1:2"]
    -    assert payload["matched_count"] == 3
    -
    -
    -@pytest.mark.asyncio
    -async def test_search_messages_returns_preview_with_source_ref(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.upsert_session(
    -        "tg:1",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    preview_lines = "\n".join(f"line-{i}" for i in range(55))
    -    store.insert_message(
    -        "tg:1",
    -        role="user",
    -        content=f"benchmark recall 0.62\n{preview_lines}",
    -        ts="2026-01-01T00:00:01+00:00",
    -        seq=0,
    -    )
    -
    -    tool = SearchMessagesTool(store)
    -    payload = json.loads(await tool.execute(query="benchmark", session_key="tg:1"))
    -
    -    assert payload["count"] == 1
    -    assert payload["matched_count"] == 1
    -    assert payload["offset"] == 0
    -    assert payload["limit"] == 10
    -    assert payload["has_more"] is False
    -    assert payload["next_offset"] is None
    -
    -    item = payload["messages"][0]
    -    assert item["id"] == "tg:1:0"
    -    assert item["source_ref"] == "tg:1:0"
    -    assert item["session_key"] == "tg:1"
    -    assert item["role"] == "user"
    -    assert item["preview_line_count"] == 50
    -    assert item["total_line_count"] == 56
    -    assert item["truncated"] is True
    -    assert "benchmark recall 0.62" in item["preview"]
    -    assert "line-48" in item["preview"]
    -    assert "line-49" not in item["preview"]
    -    assert "line-50" not in item["preview"]
    -    assert "已截断" in item["preview"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_search_messages_supports_filters(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.upsert_session(
    -        "tg:1",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    store.upsert_session(
    -        "tg:2",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -
    -    store.insert_message("tg:1", role="user", content="benchmark recall 0.62", ts="2026-01-01T00:00:01+00:00", seq=0)
    -    store.insert_message("tg:1", role="assistant", content="benchmark done", ts="2026-01-01T00:00:02+00:00", seq=1)
    -    store.insert_message("tg:2", role="user", content="benchmark other", ts="2026-01-01T00:00:03+00:00", seq=0)
    -
    -    tool = SearchMessagesTool(store)
    -
    -    payload = json.loads(
    -        await tool.execute(
    -            query="benchmark",
    -            session_key="tg:1",
    -            role="user",
    -            limit=10,
    -        )
    -    )
    -    assert payload["count"] == 1
    -    assert payload["matched_count"] == 1
    -    assert payload["messages"][0]["session_key"] == "tg:1"
    -    assert payload["messages"][0]["role"] == "user"
    -    assert payload["messages"][0]["source_ref"] == "tg:1:0"
    -    assert "0.62" in payload["messages"][0]["preview"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_search_messages_supports_offset_pagination(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.upsert_session(
    -        "tg:1",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    for seq in range(5):
    -        store.insert_message(
    -            "tg:1",
    -            role="user" if seq % 2 == 0 else "assistant",
    -            content=f"benchmark result {seq}",
    -            ts=f"2026-01-01T00:00:0{seq}+00:00",
    -            seq=seq,
    -        )
    -
    -    tool = SearchMessagesTool(store)
    -
    -    first_page = json.loads(await tool.execute(query="benchmark", session_key="tg:1", limit=2))
    -    second_page = json.loads(
    -        await tool.execute(
    -            query="benchmark",
    -            session_key="tg:1",
    -            limit=2,
    -            offset=first_page["next_offset"],
    -        )
    -    )
    -
    -    assert first_page["count"] == 2
    -    assert first_page["matched_count"] == 5
    -    assert first_page["has_more"] is True
    -    assert first_page["next_offset"] == 2
    -    assert [item["id"] for item in first_page["messages"]] == ["tg:1:4", "tg:1:3"]
    -
    -    assert second_page["count"] == 2
    -    assert second_page["matched_count"] == 5
    -    assert second_page["offset"] == 2
    -    assert second_page["has_more"] is True
    -    assert second_page["next_offset"] == 4
    -    assert [item["id"] for item in second_page["messages"]] == ["tg:1:2", "tg:1:1"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_search_messages_mixed_long_and_short_terms_keeps_short_only_hits(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.upsert_session(
    -        "tg:1",
    -        created_at="2026-01-01T00:00:00+00:00",
    -        updated_at="2026-01-01T00:00:00+00:00",
    -        metadata={},
    -    )
    -    store.insert_message("tg:1", role="user", content="phase only", ts="2026-01-01T00:00:01+00:00", seq=0)
    -    store.insert_message("tg:1", role="assistant", content="只提到支付", ts="2026-01-01T00:00:02+00:00", seq=1)
    -    store.insert_message("tg:1", role="user", content="phase 支付 一起命中", ts="2026-01-01T00:00:03+00:00", seq=2)
    -
    -    tool = SearchMessagesTool(store)
    -    payload = json.loads(await tool.execute(query="phase 支付", session_key="tg:1", limit=10))
    -
    -    assert payload["count"] == 3
    -    assert payload["matched_count"] == 3
    -    assert {item["id"] for item in payload["messages"]} == {"tg:1:0", "tg:1:1", "tg:1:2"}
    -
    -
    -@pytest.mark.asyncio
    -async def test_search_messages_empty_query_returns_empty(tmp_path):
    -    store = SessionStore(tmp_path / "sessions.db")
    -    tool = SearchMessagesTool(store)
    -    payload = json.loads(await tool.execute(query="   "))
    -    assert payload == {
    -        "count": 0,
    -        "matched_count": 0,
    -        "limit": 10,
    -        "offset": 0,
    -        "has_more": False,
    -        "next_offset": None,
    -        "messages": [],
    -    }
    -
    -
    -def test_next_seq_after_seq_zero_should_return_one(tmp_path):
    -    manager = SessionManager(tmp_path)
    -    session = manager.get_or_create("cli:test")
    -    session.messages = [
    -        {
    -            "role": "assistant",
    -            "content": "prev",
    -            "timestamp": "2026-03-27T22:04:06+08:00",
    -        }
    -    ]
    -    manager.save(session)
    -
    -    assert manager._store.next_seq("cli:test") == 1
    -
    -
    -def test_message_lookup_tools_require_fetch_for_evidence():
    -    assert "source_ref" in SearchMessagesTool.description
    -    assert "fetch_messages" in SearchMessagesTool.description
    -    assert "必须" in SearchMessagesTool.description
    -    assert "fetch_messages" in FetchMessagesTool.description
    -    assert "§cited:[" in FetchMessagesTool.description
    -
    -
    -def test_search_messages_description_requires_recall_memory_first():
    -    description = SearchMessagesTool.description
    -
    -    assert "必须先尝试 recall_memory" in description
    -    assert "recall_memory 已经尝试" in description
    -    assert "截断的原始消息" in description
    -    assert "evidence" in description
    -    assert "fetch_messages 获取完整原文" in description
    -    assert "不要把截断当作召回失败" in description
    -    assert "仍无法确定答案时" in description
    -    assert "akasha" not in description.lower()
    -
    -
    -def test_fetch_messages_description_handles_truncated_recall_preview():
    -    description = FetchMessagesTool.description
    -
    -    assert "截断的原始消息" in description
    -    assert "evidence 读取完整原文" in description
    -    assert "不要根据预览补写缺失内容" in description
    -    assert "akasha" not in description.lower()
    -
    -
    -def test_history_fact_guard_requires_fetch_after_search_preview():
    -    prompt = build_agent_behavior_rules_prompt(workspace=Path("."))
    -    assert "search_messages" in prompt
    -    assert "fetch_messages" in prompt
    -    assert "source_ref" in prompt
    -    assert "预览" in prompt
    -
    -
    -def test_memory_correction_protocol_covers_soft_corrections_and_forget_memory():
    -    prompt = build_agent_behavior_rules_prompt(workspace=Path("."))
    -    assert "其实还好" in prompt
    -    assert "并不反感" in prompt
    -    assert "forget_memory" in prompt
    -    assert "若用户这轮是在纠正你,而你本轮没有调用 `forget_memory`" in prompt
    -    assert "在拿到 fetch_messages 结果前,禁止直接调用 `forget_memory`" in prompt
    -    assert "调用了 `forget_memory` 却没有先调用 `fetch_messages`" in prompt
    -
    -
    -def test_behavior_rules_force_fact_questions_to_answer_directly():
    -    prompt = build_agent_behavior_rules_prompt(workspace=Path("."))
    -    assert "简单问题直接回答" in prompt
    -    assert "时间线、日期、安排、是否记得、列事实、重新梳理" in prompt
    -    assert "不要追加鼓励、睡觉建议、备战计划、陪伴式抚慰" in prompt
    -    assert "当前这一问如果是事实整理或时间确认,也不要顺着前文继续输出情绪安慰" in prompt
    -    assert "事实型问题答完事实就停" in prompt
    -    assert "稳住就行" in prompt
    -
    -
    -def test_behavior_rules_use_evidence_threshold_not_keyword_filtering():
    -    prompt = build_agent_behavior_rules_prompt(workspace=Path("."))
    -    assert "知识截止时间" in prompt
    -    assert "外部世界此刻是什么样" in prompt
    -    assert "本轮外部证据" in prompt
    -    assert "这里的判断看“证据门槛”,不是看字面关键词" in prompt
    -    assert "如果答案取决于稳定知识" in prompt
    -    assert "如果答案取决于本轮外部证据" in prompt
    -    assert "我现在不能确认 / 我需要先查一下" in prompt
    -    assert "没有本轮证据就只能说记忆里的旧信息" in prompt
    diff --git a/tests/test_meta_toolbox.py b/tests/test_meta_toolbox.py
    deleted file mode 100644
    index c1e1c92c1..000000000
    --- a/tests/test_meta_toolbox.py
    +++ /dev/null
    @@ -1,95 +0,0 @@
    -from typing import Any, cast
    -import pytest
    -from agent.tools.base import Tool
    -from agent.tools.filesystem import ListDirTool, ReadFileTool
    -from agent.tools.meta.register import register_common_meta_tools
    -from agent.tools.message_push import MessagePushTool
    -from agent.tools.registry import ToolRegistry
    -from agent.tools.web_fetch import WebFetchTool
    -from agent.tools.web_search import WebSearchTool
    -from bootstrap.toolsets.meta import CommonMetaToolsetProvider
    -from bootstrap.toolsets.protocol import ToolsetDeps
    -from session.store import SessionStore
    -
    -
    -def test_register_meta_tool_helpers_mark_expected_tools_always_on():
    -    tools = ToolRegistry()
    -    readonly_tools = {
    -        "web_search": WebSearchTool(),
    -        "web_fetch": WebFetchTool(requester=cast(Any, object())),
    -        "read_file": ReadFileTool(),
    -        "list_dir": ListDirTool(),
    -    }
    -
    -    push_tool = register_common_meta_tools(
    -        tools,
    -        readonly_tools,
    -        session_store=object(),
    -    )
    -    always_on = tools.get_always_on_names()
    -    assert isinstance(push_tool, MessagePushTool)
    -    assert {
    -        "tool_search",
    -        "shell",
    -        "write_stdin",
    -        "task_stop",
    -        "web_search",
    -        "web_fetch",
    -        "read_file",
    -        "list_dir",
    -        "fetch_messages",
    -        "search_messages",
    -        "message_push",
    -        "write_file",
    -        "edit_file",
    -    } <= always_on
    -    assert "request_user_confirmation" not in always_on
    -
    -
    -def test_common_meta_toolset_registers_load_skill(tmp_path):
    -    tools = ToolRegistry()
    -    session_store = SessionStore(tmp_path / "sessions.db")
    -    readonly_tools = {
    -        "web_search": WebSearchTool(),
    -        "web_fetch": WebFetchTool(requester=cast(Any, object())),
    -        "read_file": ReadFileTool(),
    -        "list_dir": ListDirTool(),
    -    }
    -
    -    result = CommonMetaToolsetProvider(readonly_tools).register(
    -        tools,
    -        ToolsetDeps(
    -            config=None,
    -            workspace=tmp_path,
    -            session_store=session_store,
    -        ),
    -    )
    -    session_store.close()
    -
    -    assert tools.has_tool("load_skill")
    -    assert "load_skill" in result.always_on_names
    -
    -
    -def test_common_meta_toolset_rejects_missing_required_dependencies(tmp_path):
    -    readonly_tools = {
    -        "web_search": cast(Any, object()),
    -        "web_fetch": cast(Any, object()),
    -        "read_file": cast(Any, object()),
    -        "list_dir": cast(Any, object()),
    -    }
    -
    -    with pytest.raises(ValueError, match="session_store"):
    -        CommonMetaToolsetProvider(readonly_tools).register(
    -            ToolRegistry(),
    -            ToolsetDeps(config=None, workspace=tmp_path),
    -        )
    -
    -    with pytest.raises(ValueError, match="web_search"):
    -        CommonMetaToolsetProvider({}).register(
    -            ToolRegistry(),
    -            ToolsetDeps(
    -                config=None,
    -                workspace=tmp_path,
    -                session_store=cast(Any, object()),
    -            ),
    -        )
    diff --git a/tests/test_mobile_pair_cli.py b/tests/test_mobile_pair_cli.py
    deleted file mode 100644
    index cd0294a9d..000000000
    --- a/tests/test_mobile_pair_cli.py
    +++ /dev/null
    @@ -1,105 +0,0 @@
    -from __future__ import annotations
    -
    -from datetime import datetime, timedelta, timezone
    -from io import StringIO
    -from pathlib import Path
    -
    -import pytest
    -
    -from scripts.akashic_release.mobile_pair import pair_mobile
    -
    -
    -def _offer(now: datetime) -> dict[str, object]:
    -    return {
    -        "protocol_version": 1,
    -        "server_id": "server-1",
    -        "server_application_key_fingerprint": "fingerprint",
    -        "server_application_public_key": "public-key",
    -        "lan_endpoints": ["wss://akashic.local:6323/ws"],
    -        "tunnel_endpoints": ["wss://mobile.huashen258.cc/ws"],
    -        "tls_spki_pins": ["pin"],
    -        "pairing_id": "pairing-1",
    -        "one_time_secret": "secret-must-only-exist-inside-qr",
    -        "expires_at": (now + timedelta(minutes=8)).isoformat(),
    -    }
    -
    -
    -def test_terminal_pairing_renders_qr_and_requires_matching_code(
    -    tmp_path: Path,
    -) -> None:
    -    now = datetime(2026, 8, 11, 2, 30, tzinfo=timezone.utc)
    -    environment = tmp_path / "runtime.env"
    -    environment.write_text("AKASHIC_PUBLISHED_WEB_PORT=2236\n", encoding="utf-8")
    -    calls: list[tuple[str, str, object]] = []
    -    status_values: list[dict[str, object]] = [
    -        {"pairing_id": "pairing-1", "status": "waiting_for_phone"},
    -        {
    -            "pairing_id": "pairing-1",
    -            "status": "waiting_for_desktop_confirmation",
    -            "device_name": "Pixel 9",
    -            "confirmation_code": "482913",
    -            "capabilities": ["stream-v1"],
    -        },
    -    ]
    -    statuses = iter(status_values)
    -
    -    def request(method: str, url: str, payload: object) -> dict[str, object]:
    -        calls.append((method, url, payload))
    -        if url.endswith("/api/chat/mobile-pairing"):
    -            return _offer(now)
    -        if method == "GET":
    -            return next(statuses)
    -        return {"device_id": "device-1", "display_name": "Pixel 9"}
    -
    -    output = StringIO()
    -    result = pair_mobile(
    -        environment,
    -        input_fn=lambda _prompt: "482913",
    -        output=output,
    -        request_json=request,
    -        sleep=lambda _seconds: None,
    -        now=lambda: now,
    -    )
    -
    -    assert result == {
    -        "status": "paired",
    -        "deviceId": "device-1",
    -        "displayName": "Pixel 9",
    -    }
    -    assert "约 8 分钟" in output.getvalue()
    -    assert "服务端确认码:482913" in output.getvalue()
    -    assert "secret-must-only-exist-inside-qr" not in output.getvalue()
    -    assert all("127.0.0.1:2236" in url for _method, url, _payload in calls)
    -    assert calls[-1][2] == {"confirmation_code": "482913"}
    -
    -
    -def test_terminal_pairing_rejects_mismatched_confirmation_without_approval(
    -    tmp_path: Path,
    -) -> None:
    -    now = datetime(2026, 8, 11, 2, 30, tzinfo=timezone.utc)
    -    environment = tmp_path / "runtime.env"
    -    environment.write_text("AKASHIC_PUBLISHED_WEB_PORT=2236\n", encoding="utf-8")
    -    calls: list[str] = []
    -
    -    def request(method: str, url: str, _payload: object) -> dict[str, object]:
    -        calls.append(method)
    -        if url.endswith("/api/chat/mobile-pairing"):
    -            return _offer(now)
    -        return {
    -            "pairing_id": "pairing-1",
    -            "status": "waiting_for_desktop_confirmation",
    -            "device_name": "Unknown phone",
    -            "confirmation_code": "482913",
    -        }
    -
    -    with pytest.raises(RuntimeError, match="未批准"):
    -        pair_mobile(
    -            environment,
    -            input_fn=lambda _prompt: "000000",
    -            output=StringIO(),
    -            request_json=request,
    -            sleep=lambda _seconds: None,
    -            now=lambda: now,
    -        )
    -
    -    assert calls == ["POST", "GET"]
    diff --git a/tests/test_model_catalog_reader.py b/tests/test_model_catalog_reader.py
    deleted file mode 100644
    index 2626a46e1..000000000
    --- a/tests/test_model_catalog_reader.py
    +++ /dev/null
    @@ -1,85 +0,0 @@
    -from __future__ import annotations
    -
    -from agent.plugin_composition import (
    -    CapabilitySources,
    -    ConnectionDescriptor,
    -    ModelAvailability,
    -    ModelCapabilities,
    -    ModelCatalogSnapshot,
    -    ModelDescriptor,
    -    ModelKind,
    -    ModelRole,
    -)
    -from agent.plugins.model_catalog import (
    -    default_chat_model_id,
    -    project_chat_runtimes,
    -)
    -
    -
    -def _catalog() -> ModelCatalogSnapshot:
    -    return ModelCatalogSnapshot(
    -        revision=9,
    -        connections=(
    -            ConnectionDescriptor(
    -                connection_id="connection-a",
    -                name="Account A",
    -                driver_id="driver-a",
    -                auth_identity="account-a",
    -                availability=ModelAvailability.AVAILABLE,
    -            ),
    -        ),
    -        models=(
    -            ModelDescriptor(
    -                model_id="chat-a",
    -                connection_id="connection-a",
    -                kind=ModelKind.CHAT,
    -                model="wire-a",
    -                default_reasoning_effort="high",
    -                capabilities=ModelCapabilities(
    -                    context_window=32_000,
    -                    supported_reasoning_efforts=("medium", "high"),
    -                ),
    -                capability_sources=CapabilitySources(context_window="catalog"),
    -                availability=ModelAvailability.AVAILABLE,
    -            ),
    -            ModelDescriptor(
    -                model_id="chat-disabled",
    -                connection_id="connection-a",
    -                kind=ModelKind.CHAT,
    -                model="wire-disabled",
    -                default_reasoning_effort=None,
    -                capabilities=ModelCapabilities(),
    -                capability_sources=CapabilitySources(),
    -                availability=ModelAvailability.DISABLED,
    -            ),
    -        ),
    -        role_bindings={ModelRole.DEFAULT: "chat-a", ModelRole.AGENT: "chat-a"},
    -        default_embedding_model_id=None,
    -    )
    -
    -
    -def test_catalog_projection_keeps_client_shape_without_unavailable_models() -> None:
    -    snapshot = _catalog()
    -    assert default_chat_model_id(snapshot) == "chat-a"
    -    assert project_chat_runtimes(snapshot) == [
    -        {
    -            "id": "chat-a",
    -            "provider": "driver-a",
    -            "catalogProvider": "driver-a",
    -            "model": "wire-a",
    -            "reasoningEffort": "high",
    -            "supportedReasoningEfforts": ["medium", "high"],
    -            "sourceId": "connection-a",
    -            "sourceName": "Account A",
    -            "contextWindow": 32_000,
    -            "maxOutputTokens": 0,
    -            "inputModalities": ["text"],
    -            "capabilitySource": "catalog",
    -            "capabilitySources": {
    -                "contextWindow": "catalog",
    -                "maxOutputTokens": "unknown",
    -                "inputModalities": "unknown",
    -            },
    -            "roles": ["agent", "default"],
    -        }
    -    ]
    diff --git a/tests/test_model_control_api.py b/tests/test_model_control_api.py
    deleted file mode 100644
    index 1ad92acb2..000000000
    --- a/tests/test_model_control_api.py
    +++ /dev/null
    @@ -1,418 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from types import SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -from fastapi.testclient import TestClient
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    MODEL_CATALOG,
    -    MODEL_SETTINGS,
    -    CapabilitySources,
    -    ConnectionDescriptor,
    -    CreateConnectionWithModel,
    -    DiscoveredModel,
    -    ModelAvailability,
    -    ModelCapabilities,
    -    ModelCatalogSnapshot,
    -    ModelDescriptor,
    -    ModelKind,
    -    ModelRole,
    -    SetDefaultModel,
    -    SettingsReceipt,
    -    UpdateConnection,
    -)
    -from agent.plugins.model_control import ModelControlUnavailable, RuntimeModelControl
    -from agent.plugins.snapshot import get_current_runtime_snapshot
    -from bootstrap.chat_api import create_chat_app
    -from infra.channels.web_chat_channel import WebChatChannel
    -
    -
    -def _catalog() -> ModelCatalogSnapshot:
    -    return ModelCatalogSnapshot(
    -        revision=7,
    -        connections=(
    -            ConnectionDescriptor(
    -                connection_id="account-a",
    -                name="Account A",
    -                driver_id="openai-compatible",
    -                auth_identity="key-a",
    -                availability=ModelAvailability.AVAILABLE,
    -            ),
    -        ),
    -        models=(
    -            ModelDescriptor(
    -                model_id="chat-a",
    -                connection_id="account-a",
    -                kind=ModelKind.CHAT,
    -                model="wire-chat",
    -                default_reasoning_effort="high",
    -                capabilities=ModelCapabilities(
    -                    context_window=64_000,
    -                    input_modalities=("text", "image"),
    -                    supports_tool_calls=True,
    -                ),
    -                capability_sources=CapabilitySources(context_window="catalog"),
    -                availability=ModelAvailability.AVAILABLE,
    -            ),
    -        ),
    -        role_bindings={ModelRole.DEFAULT: "chat-a"},
    -        default_embedding_model_id=None,
    -    )
    -
    -
    -class _Lease:
    -    def __init__(self, root: object) -> None:
    -        self.snapshot = SimpleNamespace(composition_root=root)
    -        self.active = True
    -
    -    def fork(self) -> _Lease:
    -        return self
    -
    -    async def release(self) -> None:
    -        self.active = False
    -
    -
    -@pytest.mark.asyncio
    -async def test_runtime_model_control_binds_and_releases_exact_snapshot() -> None:
    -    catalog = _catalog()
    -    commands: list[object] = []
    -
    -    class Settings:
    -        async def discover(
    -            self,
    -            connection: AddConnection,
    -        ) -> tuple[DiscoveredModel, ...]:
    -            assert get_current_runtime_snapshot() is lease.snapshot
    -            return (
    -                DiscoveredModel(
    -                    kind=ModelKind.CHAT,
    -                    model=connection.name,
    -                    capabilities=ModelCapabilities(),
    -                    capability_sources=CapabilitySources(),
    -                ),
    -            )
    -
    -        async def apply(self, command: object) -> SettingsReceipt:
    -            assert get_current_runtime_snapshot() is lease.snapshot
    -            commands.append(command)
    -            return SettingsReceipt(revision=8, status="committed")
    -
    -    def read_catalog() -> ModelCatalogSnapshot:
    -        assert get_current_runtime_snapshot() is lease.snapshot
    -        return catalog
    -
    -    services = {
    -        MODEL_CATALOG: SimpleNamespace(snapshot=read_catalog),
    -        MODEL_SETTINGS: Settings(),
    -    }
    -    root = SimpleNamespace(context=SimpleNamespace(get=services.get))
    -    lease = _Lease(root)
    -
    -    class Store:
    -        async def acquire(self) -> _Lease:
    -            lease.active = True
    -            return lease
    -
    -    control = RuntimeModelControl(cast(Any, Store()))
    -    assert await control.catalog() is catalog
    -    assert not lease.active
    -    preview = AddConnection(
    -        expected_revision=7,
    -        connection_id="preview",
    -        name="preview-model",
    -        driver_id="openai-compatible",
    -        endpoint="https://example.test/v1",
    -        auth_identity="preview",
    -        credential={"access_token": "temporary"},
    -    )
    -    assert (await control.discover(preview))[0].model == "preview-model"
    -    assert not lease.active
    -    receipt = await control.apply(SetDefaultModel(7, ModelRole.DEFAULT, "chat-a"))
    -    assert receipt.revision == 8
    -    assert commands == [SetDefaultModel(7, ModelRole.DEFAULT, "chat-a")]
    -    assert not lease.active
    -    assert get_current_runtime_snapshot() is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_runtime_model_control_reports_missing_service_and_releases() -> None:
    -    root = SimpleNamespace(context=SimpleNamespace(get=lambda _key: None))
    -    lease = _Lease(root)
    -
    -    class Store:
    -        async def acquire(self) -> _Lease:
    -            return lease
    -
    -    control = RuntimeModelControl(cast(Any, Store()))
    -    with pytest.raises(ModelControlUnavailable, match="模型目录"):
    -        await control.catalog()
    -    assert not lease.active
    -    assert get_current_runtime_snapshot() is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_cancelled_discovery_releases_runtime_snapshot() -> None:
    -    started = asyncio.Event()
    -
    -    class Settings:
    -        async def discover(self, _connection: AddConnection):
    -            started.set()
    -            await asyncio.Event().wait()
    -
    -    services = {MODEL_SETTINGS: Settings()}
    -    root = SimpleNamespace(context=SimpleNamespace(get=services.get))
    -    lease = _Lease(root)
    -
    -    class Store:
    -        async def acquire(self) -> _Lease:
    -            lease.active = True
    -            return lease
    -
    -    control = RuntimeModelControl(cast(Any, Store()))
    -    connection = AddConnection(
    -        expected_revision=0,
    -        connection_id="preview",
    -        name="Preview",
    -        driver_id="openai-compatible",
    -        endpoint="https://example.test/v1",
    -        auth_identity="preview",
    -        credential={"access_token": "temporary"},
    -    )
    -    task = asyncio.create_task(control.discover(connection))
    -    await started.wait()
    -    task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await task
    -
    -    assert not lease.active
    -    assert get_current_runtime_snapshot() is None
    -
    -
    -def test_model_settings_http_projects_catalog_and_validates_command(tmp_path) -> None:
    -    applied: list[object] = []
    -
    -    class Control:
    -        async def catalog(self) -> ModelCatalogSnapshot:
    -            return _catalog()
    -
    -        async def discover(
    -            self,
    -            connection: AddConnection,
    -        ) -> tuple[DiscoveredModel, ...]:
    -            assert connection.endpoint == "https://example.test/v1"
    -            assert connection.credential == {"access_token": "temporary"}
    -            return (
    -                DiscoveredModel(
    -                    kind=ModelKind.CHAT,
    -                    model="deepseek-v4-flash-vision",
    -                    capabilities=ModelCapabilities(
    -                        context_window=1_000_000,
    -                        input_modalities=("text", "image"),
    -                        supports_tool_calls=True,
    -                    ),
    -                    capability_sources=CapabilitySources(
    -                        context_window="litellm-remote@sha256:test",
    -                        input_modalities="litellm-remote@sha256:test",
    -                        tool_calls="litellm-remote@sha256:test",
    -                    ),
    -                    driver_config={"format_version": 1},
    -                ),
    -            )
    -
    -        async def apply(self, command: object) -> SettingsReceipt:
    -            applied.append(command)
    -            return SettingsReceipt(revision=8, status="committed")
    -
    -    app = create_chat_app(
    -        workspace=tmp_path,
    -        channel=WebChatChannel(),
    -        model_control=cast(Any, Control()),
    -    )
    -    client = TestClient(app)
    -
    -    catalog = client.get("/api/chat/model-settings/catalog")
    -    discovered = client.post(
    -        "/api/chat/model-settings/discover",
    -        json={
    -            "expected_revision": 7,
    -            "connection_id": "temporary-account",
    -            "name": "Temporary",
    -            "driver_id": "openai-compatible",
    -            "endpoint": "https://example.test/v1",
    -            "auth_identity": "temporary-account",
    -            "credential": {"access_token": "temporary"},
    -            "driver_config": {"catalog_provider_id": "deepseek"},
    -        },
    -    )
    -    response = client.post(
    -        "/api/chat/model-settings/command",
    -        json={
    -            "type": "set_default",
    -            "expected_revision": 7,
    -            "role": "vision",
    -            "model_id": "chat-a",
    -        },
    -    )
    -
    -    assert catalog.status_code == 200
    -    assert discovered.status_code == 200
    -    assert discovered.json()["models"][0] == {
    -        "kind": "chat",
    -        "model": "deepseek-v4-flash-vision",
    -        "defaultReasoningEffort": None,
    -        "capabilities": {
    -            "contextWindow": 1_000_000,
    -            "maxOutputTokens": None,
    -            "inputModalities": ["text", "image"],
    -            "supportsToolCalls": True,
    -            "supportsParallelToolCalls": None,
    -            "supportedReasoningEfforts": [],
    -            "embeddingDimensions": None,
    -            "embeddingNormalization": None,
    -        },
    -        "capabilitySources": {
    -            "contextWindow": "litellm-remote@sha256:test",
    -            "maxOutputTokens": "unknown",
    -            "inputModalities": "litellm-remote@sha256:test",
    -            "toolCalls": "litellm-remote@sha256:test",
    -            "parallelToolCalls": "unknown",
    -            "reasoningEfforts": "unknown",
    -            "embeddingDimensions": "unknown",
    -            "embeddingNormalization": "unknown",
    -        },
    -        "driverConfig": {"format_version": 1},
    -    }
    -    invalid_discovery = client.post(
    -        "/api/chat/model-settings/discover",
    -        json={
    -            "expected_revision": 7,
    -            "connection_id": "temporary-account",
    -            "name": "Temporary",
    -            "driver_id": "openai-compatible",
    -            "endpoint": "https://example.test/v1",
    -            "auth_identity": "temporary-account",
    -            "credential": {"access_token": "must-not-leak"},
    -            "unexpected": True,
    -        },
    -    )
    -    assert invalid_discovery.status_code == 422
    -    assert "must-not-leak" not in invalid_discovery.text
    -    unsupported_discovery = client.post(
    -        "/api/chat/model-settings/discover",
    -        json={
    -            "expected_revision": 7,
    -            "connection_id": "temporary-account",
    -            "name": "Temporary",
    -            "driver_id": "opencode-go",
    -            "endpoint": "https://example.test/v1",
    -            "auth_identity": "temporary-account",
    -            "credential": {"access_token": "must-not-leak"},
    -            "driver_config": {
    -                "max_retries": 1_000_000,
    -                "connect_timeout": 1_000_000,
    -                "read_timeout": 1_000_000,
    -            },
    -        },
    -    )
    -    assert unsupported_discovery.status_code == 422
    -    assert unsupported_discovery.json()["detail"] == (
    -        "模型预览仅支持 openai-compatible"
    -    )
    -    assert "must-not-leak" not in unsupported_discovery.text
    -    assert catalog.json()["connections"][0]["driverId"] == "openai-compatible"
    -    assert "endpoint" not in catalog.json()["connections"][0]
    -    assert catalog.json()["models"][0]["capabilities"]["inputModalities"] == [
    -        "text",
    -        "image",
    -    ]
    -    assert response.json() == {
    -        "revision": 8,
    -        "status": "committed",
    -        "attemptId": None,
    -        "challenge": None,
    -    }
    -    assert applied == [SetDefaultModel(7, ModelRole.VISION, "chat-a")]
    -
    -    retained = client.post(
    -        "/api/chat/model-settings/command",
    -        json={
    -            "type": "update_connection",
    -            "expected_revision": 8,
    -            "connection_id": "account-a",
    -            "name": "Renamed",
    -            "auth_identity": "key-a",
    -        },
    -    )
    -    assert retained.status_code == 200
    -    assert applied[-1] == UpdateConnection(
    -        expected_revision=8,
    -        connection_id="account-a",
    -        name="Renamed",
    -        auth_identity="key-a",
    -        endpoint=None,
    -    )
    -
    -    invalid = client.post(
    -        "/api/chat/model-settings/command",
    -        json={
    -            "type": "set_default",
    -            "expected_revision": 8,
    -            "role": "default",
    -            "model_id": "chat-a",
    -            "provider": "special-case",
    -        },
    -    )
    -    assert invalid.status_code == 422
    -
    -    secret_invalid = client.post(
    -        "/api/chat/model-settings/command",
    -        json={
    -            "type": "add_connection",
    -            "expected_revision": 8,
    -            "connection_id": "account-b",
    -            "name": "Account B",
    -            "driver_id": "openai-compatible",
    -            "endpoint": "https://example.test/v1",
    -            "auth_identity": "account-b",
    -            "credential": {"access_token": "must-not-leak"},
    -            "unexpected": True,
    -        },
    -    )
    -    assert secret_invalid.status_code == 422
    -    assert "must-not-leak" not in secret_invalid.text
    -
    -    created = client.post(
    -        "/api/chat/model-settings/command",
    -        json={
    -            "type": "create_connection_with_model",
    -            "connection": {
    -                "expected_revision": 8,
    -                "connection_id": "account-b",
    -                "name": "Account B",
    -                "driver_id": "openai-compatible",
    -                "endpoint": "https://example.test/v1",
    -                "auth_identity": "account-b",
    -                "credential": {"access_token": "safe"},
    -            },
    -            "model": {
    -                "expected_revision": 8,
    -                "model_id": "chat-b",
    -                "connection_id": "account-b",
    -                "kind": "chat",
    -                "model": "wire-b",
    -                "capabilities": {
    -                    "input_modalities": ["text", "image"],
    -                    "supported_reasoning_efforts": ["high"],
    -                },
    -                "capability_sources": {},
    -            },
    -        },
    -    )
    -    assert created.status_code == 200
    -    assert isinstance(applied[-1], CreateConnectionWithModel)
    -    assert applied[-1].model.capabilities.input_modalities == ("text", "image")
    -    assert applied[-1].model.capabilities.supported_reasoning_efforts == ("high",)
    diff --git a/tests/test_model_settings_http_receipt.py b/tests/test_model_settings_http_receipt.py
    deleted file mode 100644
    index 4546f78c6..000000000
    --- a/tests/test_model_settings_http_receipt.py
    +++ /dev/null
    @@ -1,20 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -
    -from agent.plugin_composition import SettingsReceipt
    -from agent.plugin_composition.model_settings_http import _receipt_payload
    -
    -
    -def test_auth_challenge_is_plain_json_at_http_boundary() -> None:
    -    receipt = SettingsReceipt(
    -        revision=0,
    -        status="pending",
    -        attempt_id="attempt-1",
    -        challenge={"steps": [{"name": "wait"}]},
    -    )
    -
    -    payload = _receipt_payload(receipt)
    -
    -    assert payload["challenge"] == {"steps": [{"name": "wait"}]}
    -    assert json.loads(json.dumps(payload))["challenge"] == payload["challenge"]
    diff --git a/tests/test_models_plugin_ordinary_install.py b/tests/test_models_plugin_ordinary_install.py
    deleted file mode 100644
    index db42c194e..000000000
    --- a/tests/test_models_plugin_ordinary_install.py
    +++ /dev/null
    @@ -1,789 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import hashlib
    -import os
    -import shutil
    -import sqlite3
    -import subprocess
    -import sys
    -import threading
    -import time
    -from contextlib import closing
    -from pathlib import Path
    -from typing import Any, cast
    -from unittest.mock import patch
    -
    -import pytest
    -import uvicorn
    -from fastapi.testclient import TestClient
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    AddModel,
    -    CancelConnectionAuth,
    -    CHAT_MODELS,
    -    EMBEDDINGS,
    -    MODEL_CATALOG,
    -    CapabilitySources,
    -    CreateConnectionWithModel,
    -    DiscoveredModel,
    -    ModelCapabilities,
    -    ModelKind,
    -    ModelAvailability,
    -    ModelRequest,
    -    ModelRole,
    -    ModelUnavailableError,
    -    FinishConnectionAuth,
    -    StartConnectionAuth,
    -    SetDefaultModel,
    -    SyncModels,
    -    UpdateConnection,
    -)
    -from agent.tools.vision import ReadImageVisionTool
    -from agent.plugins.install import (
    -    finalize_uninstall_plugin,
    -    install_git_plugin,
    -    set_installed_plugin_enabled,
    -)
    -from agent.plugins.dashboard_host import PluginDashboardHost
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.model_control import RuntimeModelControl
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from bootstrap.chat_api import create_chat_app
    -from bootstrap.web_runtime import chat_socket_path
    -from bootstrap.web_shell import create_web_shell_app
    -from bus.event_bus import EventBus
    -from infra.channels.web_chat_channel import WebChatChannel
    -
    -
    -class _RepositoryModelsImportBlocker:
    -    """Make repository-local models imports fail during the ordinary-plugin gate."""
    -
    -    def find_spec(
    -        self,
    -        fullname: str,
    -        path: object = None,
    -        target: object = None,
    -    ) -> None:
    -        _ = path, target
    -        if fullname == "plugins.models" or fullname.startswith("plugins.models."):
    -            raise ModuleNotFoundError(f"repository plugin import blocked: {fullname}")
    -        return None
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[Path("plugins/shell_ui")],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -def _commit(repo: Path) -> None:
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "init"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -
    -
    -def _write_fake_driver(repo: Path) -> None:
    -    repo.mkdir(parents=True)
    -    (repo / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        'name = "fake-model-driver"\n'
    -        'version = "1.0.0"\n'
    -        "api_version = 3\n"
    -        'entrypoint = "plugin.py"\n',
    -        encoding="utf-8",
    -    )
    -    (repo / "plugin.py").write_text(
    -        "from agent.plugin_composition import (\n"
    -        "  MODEL_DRIVERS, DriverConnection, EmbeddingResult, LLMResponse,\n"
    -        "  CapabilitySources, DiscoveredModel, ModelCapabilities, ModelContinuation,\n"
    -        "  ModelDriverDefinition, ModelKind,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'fake-model-driver'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (MODEL_DRIVERS,)\n"
    -        "workspace_roots = ()\n"
    -        "workspace_files = ()\n"
    -        "opened_configs = []\n"
    -        "chat_requests = []\n"
    -        "cancel_calls = []\n"
    -        "finish_started = None\n"
    -        "finish_continue = None\n"
    -        "class Chat:\n"
    -        "  def __init__(self, descriptor): self.descriptor = descriptor\n"
    -        "  async def complete(self, request):\n"
    -        "    chat_requests.append(request)\n"
    -        "    return LLMResponse(content='ok', continuation=ModelContinuation(\n"
    -        "      self.descriptor.binding_id, {'step': 1}))\n"
    -        "  def estimate_context_tokens(self, messages, tools=()):\n"
    -        "    return len(messages) + len(tools)\n"
    -        "  def estimate_appended_message_tokens(self, messages):\n"
    -        "    return len(messages)\n"
    -        "  @property\n"
    -        "  def max_tool_schemas(self): return 64\n"
    -        "class Embedding:\n"
    -        "  def __init__(self, descriptor): self.descriptor = descriptor\n"
    -        "  async def embed(self, texts):\n"
    -        "    return EmbeddingResult(tuple(\n"
    -        "      ((1.0, 0.0) if text == 'wrong' else (1.0, 0.0, 0.0))\n"
    -        "      for text in texts))\n"
    -        "async def open_driver(connection, credential):\n"
    -        "  opened_configs.append(dict(connection.config))\n"
    -        "  secret = await credential.read()\n"
    -        "  assert secret['access_token'] == 'secret'\n"
    -        "  return DriverConnection(lambda d, c: Chat(d), lambda d, c: Embedding(d))\n"
    -        "async def discover(connection, credential):\n"
    -        "  await credential.read()\n"
    -        "  return (DiscoveredModel(kind=ModelKind.CHAT, model='fake-chat-wire',\n"
    -        "    capabilities=ModelCapabilities(context_window=8192),\n"
    -        "    capability_sources=CapabilitySources(context_window='fake-catalog'),\n"
    -        "    driver_config={'catalog': 'refreshed'}),)\n"
    -        "async def start_auth(input):\n"
    -        "  state = {'poll': 0}\n"
    -        "  if input.get('block') == '1': state['block'] = True\n"
    -        "  return {'state': state, 'challenge': {'code': {'value': 'abc'}}}\n"
    -        "async def finish_auth(state):\n"
    -        "  if state.get('block'):\n"
    -        "    finish_started.set()\n"
    -        "    await finish_continue.wait()\n"
    -        "  poll = state['poll']\n"
    -        "  if poll < 2:\n"
    -        "    return {'status': 'pending', 'state': {'poll': poll + 1},\n"
    -        "            'challenge': {'poll': poll + 1}}\n"
    -        "  return {'status': 'complete', 'name': 'OAuth',\n"
    -        "          'endpoint': 'https://oauth.example.test/v1',\n"
    -        "          'auth_identity': 'oauth-account',\n"
    -        "          'credential': {'driver': 'api_key', 'access_token': 'secret'},\n"
    -        "          'driver_config': {}}\n"
    -        "async def cancel_auth(state):\n"
    -        "  cancel_calls.append(dict(state))\n"
    -        "  if len(cancel_calls) == 1: raise RuntimeError('temporary cancel failure')\n"
    -        "async def apply(ctx, config):\n"
    -        "  drivers = ctx.require(MODEL_DRIVERS)\n"
    -        "  await drivers.register(ctx, ModelDriverDefinition(\n"
    -        "    driver_id='fake', contract_version='1', open=open_driver, discover=discover,\n"
    -        "    start_auth=start_auth, finish_auth=finish_auth, cancel_auth=cancel_auth))\n",
    -        encoding="utf-8",
    -    )
    -
    -
    -async def _configure_and_call(manager: PluginManager, workspace: Path) -> None:
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    root = snapshot.composition_root
    -    control = RuntimeModelControl(manager.snapshot_store)
    -    with pytest.raises(ModelUnavailableError, match="维度"):
    -        await control.apply(
    -            CreateConnectionWithModel(
    -                connection=AddConnection(
    -                    expected_revision=0,
    -                    connection_id="failed-connection",
    -                    name="Failed",
    -                    driver_id="fake",
    -                    endpoint="https://example.test/v1",
    -                    auth_identity="failed-account",
    -                    credential={"driver": "api_key", "access_token": "secret"},
    -                ),
    -                model=AddModel(
    -                    expected_revision=0,
    -                    model_id="failed-embedding",
    -                    connection_id="failed-connection",
    -                    kind=ModelKind.EMBEDDING,
    -                    model="fake-embedding-wire",
    -                    capabilities=ModelCapabilities(embedding_dimensions=2),
    -                    capability_sources=CapabilitySources(embedding_dimensions="test"),
    -                ),
    -            )
    -        )
    -    failed_catalog = await control.catalog()
    -    assert failed_catalog.revision == 0
    -    assert failed_catalog.connections == () and failed_catalog.models == ()
    -    assert (workspace / "model-registry.sqlite3").is_file()
    -    revision = (
    -        await control.apply(
    -            AddConnection(
    -                expected_revision=0,
    -                connection_id="fake-connection",
    -                name="Fake",
    -                driver_id="fake",
    -                endpoint="https://example.test/v1",
    -                auth_identity="fake-account",
    -                credential={"driver": "api_key", "access_token": "secret"},
    -                driver_config={"nested": {"mode": "old"}},
    -            )
    -        )
    -    ).revision
    -    revision = (
    -        await control.apply(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="fake-chat",
    -                connection_id="fake-connection",
    -                kind=ModelKind.CHAT,
    -                model="fake-chat-wire",
    -                capabilities=ModelCapabilities(
    -                    context_window=4096,
    -                    max_output_tokens=512,
    -                    supports_tool_calls=True,
    -                ),
    -                capability_sources=CapabilitySources(
    -                    context_window="test",
    -                    max_output_tokens="test",
    -                    tool_calls="test",
    -                ),
    -            )
    -        )
    -    ).revision
    -    revision = (
    -        await control.apply(
    -            SetDefaultModel(
    -                expected_revision=revision,
    -                role=ModelRole.DEFAULT,
    -                model_id="fake-chat",
    -            )
    -        )
    -    ).revision
    -    with pytest.raises(ModelUnavailableError, match="维度"):
    -        await control.apply(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="wrong-embedding",
    -                connection_id="fake-connection",
    -                kind=ModelKind.EMBEDDING,
    -                model="fake-embedding-wire",
    -                capabilities=ModelCapabilities(embedding_dimensions=2),
    -                capability_sources=CapabilitySources(embedding_dimensions="test"),
    -            )
    -        )
    -    assert (await control.catalog()).revision == revision
    -    revision = (
    -        await control.apply(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="fake-embedding",
    -                connection_id="fake-connection",
    -                kind=ModelKind.EMBEDDING,
    -                model="fake-embedding-wire",
    -                capabilities=ModelCapabilities(
    -                    embedding_dimensions=3,
    -                    embedding_normalization="none",
    -                ),
    -                capability_sources=CapabilitySources(
    -                    embedding_dimensions="test",
    -                    embedding_normalization="test",
    -                ),
    -            )
    -        )
    -    ).revision
    -    revision = (
    -        await control.apply(
    -            SetDefaultModel(
    -                expected_revision=revision,
    -                role=None,
    -                model_id="fake-embedding",
    -            )
    -        )
    -    ).revision
    -    assert revision == 5
    -
    -    synced = await control.apply(
    -        SyncModels(expected_revision=revision, connection_id="fake-connection")
    -    )
    -    revision = synced.revision
    -    assert revision == 5
    -    synced_model = root.context.require(MODEL_CATALOG).snapshot().model("fake-chat")
    -    assert synced_model.capabilities.context_window == 4096
    -
    -    with pytest.raises(ValueError, match="image-capable"):
    -        await control.apply(
    -            SetDefaultModel(
    -                expected_revision=revision,
    -                role=ModelRole.VISION,
    -                model_id="fake-chat",
    -            )
    -        )
    -    revision = (
    -        await control.apply(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="fake-vision",
    -                connection_id="fake-connection",
    -                kind=ModelKind.CHAT,
    -                model="fake-vision-wire",
    -                capabilities=ModelCapabilities(input_modalities=("text", "image")),
    -                capability_sources=CapabilitySources(input_modalities="test"),
    -            )
    -        )
    -    ).revision
    -    revision = (
    -        await control.apply(
    -            SetDefaultModel(
    -                expected_revision=revision,
    -                role=ModelRole.VISION,
    -                model_id="fake-vision",
    -            )
    -        )
    -    ).revision
    -    assert revision == 7
    -
    -    lease = await manager._snapshot_store.acquire()
    -    token = bind_runtime_snapshot(lease)
    -    try:
    -        chat_models = root.context.require(CHAT_MODELS)
    -        async with chat_models.execution() as execution:
    -            chat = execution.chat(ModelRole.DEFAULT)
    -            response = await chat.complete(
    -                ModelRequest(messages=({"role": "user", "content": "hi"},))
    -            )
    -            assert response.content == "ok"
    -            assert response.continuation is not None
    -            assert response.continuation.binding_id == chat.descriptor.binding_id
    -            vision = execution.chat(ModelRole.VISION)
    -            image = workspace / "vision.png"
    -            image.write_bytes(b"fixture")
    -            with patch(
    -                "agent.tools.vision.encode_image_data_uri",
    -                return_value="data:image/png;base64,AA==",
    -            ):
    -                assert (
    -                    await ReadImageVisionTool().execute(str(image), "describe") == "ok"
    -                )
    -            assert (
    -                vision.descriptor.plugin_snapshot_id
    -                == chat.descriptor.plugin_snapshot_id
    -            )
    -            assert (
    -                vision.descriptor.model_revision
    -                == chat.descriptor.model_revision
    -                == revision
    -            )
    -            driver_generation = manager.generation("fake-model-driver@ordinary-test")
    -            assert driver_generation is not None
    -            request = driver_generation.instance.module.chat_requests[-1]
    -            assert isinstance(request, ModelRequest)
    -            assert request.messages[0]["content"][1]["type"] == "image_url"
    -        embeddings = root.context.require(EMBEDDINGS)
    -        described = embeddings.describe()
    -        async with chat_models.execution():
    -            async with embeddings.bind() as embedding:
    -                assert embedding.descriptor.identity == described.identity
    -            with pytest.raises(ModelUnavailableError, match="不可用"):
    -                async with embeddings.bind(model_id="another-embedding"):
    -                    pass
    -        async with embeddings.bind() as embedding:
    -            assert embedding.descriptor.identity == described.identity
    -            result = await embedding.embed(("hello",))
    -            assert result.vectors == ((1.0, 0.0, 0.0),)
    -            with pytest.raises(ModelUnavailableError, match="维度"):
    -                await embedding.embed(("wrong",))
    -
    -        async with chat_models.execution() as parent_execution:
    -
    -            async def inherited_chat_child() -> None:
    -                async with root.context.runtime_scope():
    -                    async with chat_models.execution():
    -                        pass
    -
    -            with pytest.raises(RuntimeError, match="不能由子 task 继承"):
    -                await asyncio.create_task(inherited_chat_child())
    -
    -            async def independent_child() -> object:
    -                async with root.context.runtime_scope():
    -                    async with chat_models.independent_execution() as execution:
    -                        return execution
    -
    -            child_execution = await asyncio.create_task(independent_child())
    -            assert child_execution is not parent_execution
    -
    -        child_ready = asyncio.Event()
    -        child_continue = asyncio.Event()
    -
    -        async with chat_models.execution():
    -
    -            async def inherited_child() -> None:
    -                child_ready.set()
    -                await child_continue.wait()
    -                async with embeddings.bind():
    -                    pass
    -
    -            child = asyncio.create_task(inherited_child())
    -            await child_ready.wait()
    -        child_continue.set()
    -        with pytest.raises(RuntimeError, match="不能由子 task 继承"):
    -            await child
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -
    -    # Core 给后台操作绑定同一 Root 的短 lease;退出后不长期占用 generation。
    -    async with root.context.runtime_scope():
    -        async with root.context.require(EMBEDDINGS).bind() as embedding:
    -            assert embedding.descriptor.identity == described.identity
    -
    -    updated = await control.apply(
    -        UpdateConnection(
    -            expected_revision=revision,
    -            connection_id="fake-connection",
    -            name="Fake",
    -            auth_identity="fake-account",
    -            endpoint=None,
    -            driver_config={},
    -        )
    -    )
    -    revision = updated.revision
    -    driver_generation = manager.generation("fake-model-driver@ordinary-test")
    -    assert driver_generation is not None
    -    assert driver_generation.instance.module.opened_configs[-1] == {}
    -    with closing(sqlite3.connect(workspace / "model-registry.sqlite3")) as connection:
    -        endpoint = connection.execute(
    -            "SELECT base_url FROM model_connections WHERE id = 'fake-connection'"
    -        ).fetchone()
    -    assert endpoint == ("https://example.test/v1",)
    -
    -    started = await control.apply(
    -        StartConnectionAuth(
    -            driver_id="fake",
    -            connection_id="oauth-connection",
    -        )
    -    )
    -    assert started.attempt_id is not None and started.challenge is not None
    -    with pytest.raises(TypeError):
    -        started.challenge["code"]["value"] = "changed"  # type: ignore[index]
    -    for expected_poll in (1, 2):
    -        pending = await control.apply(
    -            FinishConnectionAuth(
    -                expected_revision=revision,
    -                attempt_id=started.attempt_id,
    -            )
    -        )
    -        assert pending.status == "pending"
    -        assert pending.challenge == {"poll": expected_poll}
    -    completed = await control.apply(
    -        FinishConnectionAuth(
    -            expected_revision=revision,
    -            attempt_id=started.attempt_id,
    -        )
    -    )
    -    assert completed.status == "committed"
    -    assert completed.revision == revision + 1
    -
    -    cancel_started = await control.apply(
    -        StartConnectionAuth(driver_id="fake", connection_id="cancel-connection")
    -    )
    -    assert cancel_started.attempt_id is not None
    -    cancel = CancelConnectionAuth(attempt_id=cancel_started.attempt_id)
    -    with pytest.raises(RuntimeError, match="temporary cancel failure"):
    -        await control.apply(cancel)
    -    cancelled = await control.apply(cancel)
    -    assert cancelled.status == "cancelled"
    -    assert driver_generation.instance.module.cancel_calls == [
    -        {"poll": 0},
    -        {"poll": 0},
    -    ]
    -
    -    driver_module = driver_generation.instance.module
    -    driver_module.finish_started = asyncio.Event()
    -    driver_module.finish_continue = asyncio.Event()
    -    racing = await control.apply(
    -        StartConnectionAuth(
    -            driver_id="fake",
    -            connection_id="cancel-during-finish",
    -            input={"block": "1"},
    -        )
    -    )
    -    assert racing.attempt_id is not None
    -    finishing = asyncio.create_task(
    -        control.apply(FinishConnectionAuth(revision, racing.attempt_id))
    -    )
    -    await driver_module.finish_started.wait()
    -    cancelling = asyncio.create_task(
    -        control.apply(CancelConnectionAuth(racing.attempt_id))
    -    )
    -    await asyncio.sleep(0)
    -    driver_module.finish_continue.set()
    -    with pytest.raises(ValueError, match="已取消"):
    -        await finishing
    -    assert (await cancelling).status == "cancelled"
    -    assert all(
    -        connection.connection_id != "cancel-during-finish"
    -        for connection in (await control.catalog()).connections
    -    )
    -
    -
    -def _exercise_public_model_control(manager: PluginManager, tmp_path: Path) -> None:
    -    """Cross 2236 and a real UDS before changing one installed-plugin binding."""
    -
    -    workspace = tmp_path / "workspace"
    -    socket_path = chat_socket_path(workspace)
    -    socket_path.parent.mkdir(parents=True, exist_ok=True)
    -    control = RuntimeModelControl(manager.snapshot_store)
    -    chat_app = create_chat_app(
    -        workspace=workspace,
    -        channel=WebChatChannel(),
    -        model_control=cast(Any, control),
    -    )
    -    server = uvicorn.Server(
    -        uvicorn.Config(
    -            chat_app,
    -            uds=str(socket_path),
    -            log_level="critical",
    -            access_log=False,
    -            ws="none",
    -        )
    -    )
    -    thread = threading.Thread(
    -        target=lambda: asyncio.run(server.serve()),
    -        name="ordinary-model-control-uds",
    -        daemon=True,
    -    )
    -    thread.start()
    -    deadline = time.monotonic() + 5
    -    while not socket_path.is_socket() and thread.is_alive():
    -        if time.monotonic() >= deadline:
    -            break
    -        time.sleep(0.01)
    -    assert socket_path.is_socket()
    -    try:
    -        shell = create_web_shell_app(tmp_path / "config.toml", workspace)
    -        with TestClient(shell) as client:
    -            before = client.get("/api/settings/model/catalog")
    -            retired_memory = client.post(
    -                "/api/settings/memory",
    -                headers={
    -                    "Origin": "http://testserver",
    -                    "X-Akasic-CSRF": "1",
    -                },
    -                json={
    -                    "enabled": True,
    -                    "embedding_model_id": "fake-embedding",
    -                },
    -            )
    -            changed = client.post(
    -                "/api/settings/model/command",
    -                headers={
    -                    "Origin": "http://testserver",
    -                    "X-Akasic-CSRF": "1",
    -                },
    -                json={
    -                    "type": "set_default",
    -                    "expected_revision": 9,
    -                    "role": "fast",
    -                    "model_id": "fake-chat",
    -                },
    -            )
    -        assert before.status_code == 200
    -        assert before.json()["revision"] == 9
    -        assert before.json()["models"][0]["id"] == "fake-chat"
    -        assert retired_memory.status_code == 404
    -        assert not (tmp_path / "config.toml").exists()
    -        assert changed.status_code == 200
    -        assert changed.json()["revision"] == 10
    -    finally:
    -        server.should_exit = True
    -        thread.join(timeout=5)
    -    assert not thread.is_alive()
    -
    -
    -@pytest.mark.asyncio
    -async def test_models_plugin_installs_and_runs_without_builtin_source(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    models_repo = tmp_path / "models-repo"
    -    shutil.copytree(Path("plugins/models"), models_repo)
    -    shutil.rmtree(models_repo / "__pycache__", ignore_errors=True)
    -    _commit(models_repo)
    -    driver_repo = tmp_path / "driver-repo"
    -    _write_fake_driver(driver_repo)
    -    _commit(driver_repo)
    -
    -    models_install = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(models_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    driver_install = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(driver_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    for module_name in tuple(sys.modules):
    -        if module_name == "plugins.models" or module_name.startswith("plugins.models."):
    -            monkeypatch.delitem(sys.modules, module_name)
    -    monkeypatch.setattr(
    -        sys,
    -        "meta_path",
    -        [_RepositoryModelsImportBlocker(), *sys.meta_path],
    -    )
    -    before_repo_modules = {
    -        name for name in sys.modules if name.startswith("plugins.models")
    -    }
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -
    -    generation = manager.generation("models@ordinary-test")
    -    assert generation is not None
    -    assert generation.source_type == "installed"
    -    assert generation.plugin_dir == models_install.installed_path
    -    contract_bytes = Path("packages/akashic-models-ui-v1/contract.json").read_bytes()
    -    assert dict(generation.instance.web_contract_digests) == {
    -        "models.connection-types.v1": hashlib.sha256(contract_bytes).hexdigest()
    -    }
    -    assert {
    -        name for name in sys.modules if name.startswith("plugins.models")
    -    } == before_repo_modules
    -    package = generation.instance.module.__package__
    -    assert package
    -    installed_modules = [
    -        module
    -        for module_name, module in sys.modules.items()
    -        if module_name == package or module_name.startswith(f"{package}.")
    -    ]
    -    assert installed_modules
    -    for module in installed_modules:
    -        module_file = module.__file__
    -        if module_file is not None:
    -            assert (
    -                Path(module_file)
    -                .resolve()
    -                .is_relative_to(models_install.installed_path)
    -            )
    -    driver_generation = manager.generation("fake-model-driver@ordinary-test")
    -    assert driver_generation is not None
    -    assert driver_generation.source_type == "installed"
    -    assert driver_generation.plugin_dir == driver_install.installed_path
    -    driver_module_file = driver_generation.instance.module.__file__
    -    assert driver_module_file is not None
    -    assert (
    -        Path(driver_module_file).resolve().is_relative_to(driver_install.installed_path)
    -    )
    -    dashboard_host = PluginDashboardHost(core_routes=())
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    dashboard_host.prepare_initial_snapshot(snapshot)
    -    manager.bind_dashboard_preparer(
    -        dashboard_host.prepare_snapshot,
    -        validation_releaser=dashboard_host.release_validation,
    -    )
    -    assert [
    -        route.path
    -        for binding in snapshot.dashboard_bindings
    -        for route in binding.routes  # type: ignore[attr-defined]
    -    ] == [
    -        "/api/dashboard/models/catalog",
    -        "/api/dashboard/models/discover",
    -        "/api/dashboard/models/command",
    -    ]
    -    await _configure_and_call(manager, tmp_path / "workspace")
    -    await asyncio.to_thread(_exercise_public_model_control, manager, tmp_path)
    -
    -    registry = tmp_path / "workspace/model-registry.sqlite3"
    -    registry_before = registry.read_bytes()
    -    manifest = models_repo / "akashic.plugin.toml"
    -    manifest.write_text(
    -        manifest.read_text(encoding="utf-8").replace(
    -            'version = "1.0.0"', 'version = "1.0.1"'
    -        ),
    -        encoding="utf-8",
    -    )
    -    plugin_source = models_repo / "plugin.py"
    -    plugin_source.write_text(
    -        plugin_source.read_text(encoding="utf-8").replace(
    -            'version = "1.0.0"', 'version = "1.0.1"'
    -        ),
    -        encoding="utf-8",
    -    )
    -    _commit(models_repo)
    -    upgraded = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(models_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -        stage_candidate=True,
    -    )
    -    _ = await manager.reconcile_changed()
    -    status = manager.candidate_status()
    -    assert status["candidate_plugin_id"] == "models@ordinary-test"
    -    assert status["candidate_state"] == "latest_ready", status
    -    assert registry.read_bytes() == registry_before
    -    _ = await manager.switch_ready("models@ordinary-test")
    -    current = manager.generation("models@ordinary-test")
    -    assert current is not None and current.plugin_dir == upgraded.installed_path
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    catalog = snapshot.composition_root.context.require(MODEL_CATALOG).snapshot()
    -    assert catalog.revision == 10
    -    assert registry.read_bytes() == registry_before
    -    await manager.terminate_all()
    -
    -    reloaded = _manager(tmp_path)
    -    await reloaded.load_all()
    -    snapshot = reloaded.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    catalog = snapshot.composition_root.context.require(MODEL_CATALOG).snapshot()
    -    assert catalog.revision == 10
    -    assert catalog.role_bindings[ModelRole.DEFAULT] == "fake-chat"
    -    assert catalog.role_bindings[ModelRole.FAST] == "fake-chat"
    -    assert catalog.role_bindings[ModelRole.VISION] == "fake-vision"
    -    assert catalog.default_embedding_model_id == "fake-embedding"
    -    await reloaded.terminate_all()
    -
    -    set_installed_plugin_enabled(
    -        "fake-model-driver@ordinary-test",
    -        enabled=False,
    -        plugins_home=tmp_path / "home",
    -    )
    -    _ = finalize_uninstall_plugin(
    -        "fake-model-driver@ordinary-test",
    -        workspace=tmp_path / "workspace",
    -        plugins_home=tmp_path / "home",
    -    )
    -    without_driver = _manager(tmp_path)
    -    await without_driver.load_all()
    -    snapshot = without_driver.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    catalog = snapshot.composition_root.context.require(MODEL_CATALOG).snapshot()
    -    assert catalog.revision == 10
    -    assert all(
    -        model.availability is ModelAvailability.DRIVER_UNAVAILABLE
    -        for model in catalog.models
    -    )
    -    await without_driver.terminate_all()
    -
    -    _ = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(driver_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    restored = _manager(tmp_path)
    -    await restored.load_all()
    -    snapshot = restored.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    assert all(
    -        model.availability is ModelAvailability.AVAILABLE
    -        for model in snapshot.composition_root.context.require(MODEL_CATALOG)
    -        .snapshot()
    -        .models
    -    )
    -    await restored.terminate_all()
    diff --git a/tests/test_models_plugin_store.py b/tests/test_models_plugin_store.py
    deleted file mode 100644
    index 49aa336ad..000000000
    --- a/tests/test_models_plugin_store.py
    +++ /dev/null
    @@ -1,789 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import stat
    -import sqlite3
    -import subprocess
    -import sys
    -from contextlib import closing
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    AddModel,
    -    CapabilitySources,
    -    DisableConnection,
    -    DiscoveredModel,
    -    ModelCapabilities,
    -    ModelDriverDefinition,
    -    ModelKind,
    -    ModelUnavailableError,
    -    RevisionConflictError,
    -    StartConnectionAuth,
    -)
    -from agent.model_runtime.auth.store import Credential
    -from agent.model_runtime.store import ModelRegistryStore
    -from plugins.openai_compatible.driver import definition as openai_driver_definition
    -from plugins.models.store import ModelsStore
    -from plugins.models.state import ModelsState
    -import plugins.models.state as models_state_module
    -
    -
    -@pytest.mark.asyncio
    -async def test_abandoned_auth_attempt_expires_without_saving_credentials(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    cancelled = asyncio.Event()
    -
    -    async def open_driver(*_args: object) -> Any:
    -        return object()
    -
    -    async def start_auth(_input: object) -> dict[str, object]:
    -        return {"state": {"token": "pending"}, "challenge": {"code": "wait"}}
    -
    -    async def cancel_auth(state: object) -> dict[str, object]:
    -        assert state == {"token": "pending"}
    -        cancelled.set()
    -        return {}
    -
    -    store = ModelsStore(
    -        tmp_path / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "backups",
    -    )
    -    store.initialize()
    -    state = ModelsState(store, root_instance_token=object())
    -    state._driver_registrations["fake"] = ModelDriverDefinition(  # noqa: SLF001
    -        driver_id="fake",
    -        contract_version="1",
    -        open=open_driver,
    -        start_auth=start_auth,
    -        cancel_auth=cancel_auth,
    -    )
    -    await state.seal(None)  # type: ignore[arg-type]
    -    monkeypatch.setattr(models_state_module, "_AUTH_ATTEMPT_TTL_SECONDS", 0)
    -
    -    receipt = await state._start_auth(  # noqa: SLF001
    -        StartConnectionAuth(driver_id="fake", connection_id="future-connection")
    -    )
    -    await asyncio.wait_for(cancelled.wait(), timeout=1)
    -
    -    assert receipt.attempt_id not in state._auth_attempts  # noqa: SLF001
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None
    -    assert snapshot.revision == 0 and not snapshot.connections
    -
    -
    -def _connection(
    -    revision: int,
    -    connection_id: str,
    -    *,
    -    token: str,
    -    endpoint: str = "https://example.test/v1",
    -) -> AddConnection:
    -    return AddConnection(
    -        expected_revision=revision,
    -        connection_id=connection_id,
    -        name=connection_id,
    -        driver_id="fake",
    -        endpoint=endpoint,
    -        auth_identity="shared-account",
    -        credential={"driver": "api_key", "access_token": token},
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_unsaved_discovery_uses_catalog_provider_without_persisting(
    -    tmp_path: Path,
    -) -> None:
    -    seen_provider_ids: list[str] = []
    -
    -    async def discover(_connection: object, credential: object):
    -        assert await credential.read() == {  # type: ignore[attr-defined]
    -            "driver": "api_key",
    -            "access_token": "temporary",
    -        }
    -        return (
    -            DiscoveredModel(
    -                kind=ModelKind.CHAT,
    -                model="future-vision",
    -                capabilities=ModelCapabilities(),
    -                capability_sources=CapabilitySources(),
    -            ),
    -        )
    -
    -    class CapabilityCatalog:
    -        async def enrich(self, models, *, provider_id: str):
    -            seen_provider_ids.append(provider_id)
    -            return (
    -                DiscoveredModel(
    -                    kind=models[0].kind,
    -                    model=models[0].model,
    -                    capabilities=ModelCapabilities(input_modalities=("text", "image")),
    -                    capability_sources=CapabilitySources(input_modalities="catalog"),
    -                ),
    -            )
    -
    -    store = ModelsStore(
    -        tmp_path / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "backups",
    -    )
    -    store.initialize()
    -    state = ModelsState(
    -        store,
    -        root_instance_token=object(),
    -        capability_catalog=CapabilityCatalog(),  # type: ignore[arg-type]
    -    )
    -    state._driver_registrations["fake"] = ModelDriverDefinition(  # noqa: SLF001
    -        driver_id="fake",
    -        contract_version="1",
    -        open=lambda *_args: None,  # type: ignore[arg-type]
    -        discover=discover,
    -    )
    -    await state.seal(None)  # type: ignore[arg-type]
    -    connection = AddConnection(
    -        expected_revision=0,
    -        connection_id="preview",
    -        name="Preview",
    -        driver_id="fake",
    -        endpoint="https://example.test/v1",
    -        auth_identity="preview",
    -        credential={"driver": "api_key", "access_token": "temporary"},
    -        driver_config={"catalog_provider_id": "deepseek"},
    -    )
    -
    -    discovered = await state._discover_new_connection(connection)  # noqa: SLF001
    -
    -    assert discovered[0].capabilities.input_modalities == ("text", "image")
    -    assert seen_provider_ids == ["deepseek"]
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None
    -    assert snapshot.revision == 0 and not snapshot.connections and not snapshot.models
    -
    -
    -@pytest.mark.asyncio
    -async def test_saved_model_service_rejects_another_runtime_snapshot(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    state = ModelsState(
    -        ModelsStore(
    -            tmp_path / "model-registry.sqlite3",
    -            backup_dir=tmp_path / "backups",
    -        ),
    -        root_instance_token=object(),
    -    )
    -
    -    class Lease:
    -        def __init__(self, service: object) -> None:
    -            context = SimpleNamespace(
    -                root_instance_token=state.root_instance_token,
    -                get=lambda _key: service,
    -            )
    -            self.snapshot = SimpleNamespace(
    -                snapshot_id="other",
    -                composition_root=SimpleNamespace(context=context),
    -            )
    -            self.released = False
    -
    -        async def release(self) -> None:
    -            self.released = True
    -
    -    lease = Lease(object())
    -    monkeypatch.setattr(
    -        models_state_module,
    -        "lease_current_runtime_snapshot",
    -        lambda: lease,
    -    )
    -
    -    with pytest.raises(RuntimeError, match="不属于当前 runtime snapshot"):
    -        async with state.chat_models.execution():
    -            pass
    -    assert lease.released is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_model_service_requires_owner_task_snapshot_lease(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    state = ModelsState(
    -        ModelsStore(
    -            tmp_path / "model-registry.sqlite3",
    -            backup_dir=tmp_path / "backups",
    -        ),
    -        root_instance_token=object(),
    -    )
    -    monkeypatch.setattr(
    -        models_state_module,
    -        "lease_current_runtime_snapshot",
    -        lambda: None,
    -    )
    -
    -    with pytest.raises(RuntimeError, match="当前 task"):
    -        async with state.embeddings.bind():
    -            pass
    -
    -
    -@pytest.mark.asyncio
    -async def test_legacy_openai_provider_ids_upgrade_to_ordinary_driver(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    path = workspace / "model-registry.sqlite3"
    -    legacy = ModelRegistryStore(path)
    -    revision = legacy.replace_from_llm_config(
    -        {
    -            "main": "openai-chat",
    -            "fast": "deepseek-chat",
    -            "agent": "qwen-chat",
    -            "runtimes": {
    -                "openai-chat": {
    -                    "provider": "openai",
    -                    "model": "gpt-test",
    -                    "source_id": "openai-source",
    -                    "auth": "openai-auth",
    -                    "base_url": "https://api.openai.com/v1",
    -                },
    -                "deepseek-chat": {
    -                    "provider": "deepseek",
    -                    "model": "deepseek-test",
    -                    "source_id": "deepseek-source",
    -                    "auth": "deepseek-auth",
    -                    "base_url": "https://api.deepseek.com/v1",
    -                },
    -                "qwen-chat": {
    -                    "provider": "qwen",
    -                    "model": "qwen-test",
    -                    "source_id": "qwen-source",
    -                    "auth": "qwen-auth",
    -                    "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
    -                },
    -            },
    -        },
    -        credentials={
    -            auth_id: Credential(driver="api_key", access_token="secret")
    -            for auth_id in ("openai-auth", "deepseek-auth", "qwen-auth")
    -        },
    -    )
    -    assert revision == 1
    -
    -    store = ModelsStore(
    -        path,
    -        backup_dir=workspace / "runtime" / "model-backups",
    -    )
    -    store.initialize()
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None
    -    assert {connection.driver_id for connection in snapshot.connections.values()} == {
    -        "openai-compatible"
    -    }
    -
    -    state = ModelsState(store, root_instance_token=object())
    -    definition = openai_driver_definition()
    -    state._driver_registrations[definition.driver_id] = definition  # noqa: SLF001
    -    await state.seal(None)  # type: ignore[arg-type]
    -    assert all(
    -        connection.availability.value == "available"
    -        for connection in state.catalog_snapshot().connections
    -    )
    -    assert len(tuple(store.backup_dir.glob("*.sqlite3"))) == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_historical_text_vision_binding_fails_before_driver_open(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(
    -        _connection(
    -            0,
    -            "connection",
    -            token="one",
    -            endpoint="https://user:public-catalog-secret@example.test/v1?key=hidden",
    -        )
    -    )
    -    _ = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="text-chat",
    -            connection_id="connection",
    -            kind=ModelKind.CHAT,
    -            model="text-wire",
    -            capabilities=ModelCapabilities(input_modalities=("text",)),
    -            capability_sources=CapabilitySources(input_modalities="legacy"),
    -        )
    -    )
    -    with closing(sqlite3.connect(store.path)) as connection:
    -        connection.execute(
    -            "INSERT INTO model_role_bindings(role, model_id, reasoning_effort) "
    -            "VALUES ('vision', 'text-chat', '')"
    -        )
    -        connection.commit()
    -
    -    opens = 0
    -
    -    async def open_driver(*_args: object) -> Any:
    -        nonlocal opens
    -        opens += 1
    -        return object()
    -
    -    state = ModelsState(store, root_instance_token=object())
    -    state._driver_registrations["fake"] = ModelDriverDefinition(  # noqa: SLF001
    -        driver_id="fake",
    -        contract_version="1",
    -        open=open_driver,
    -    )
    -
    -    with pytest.raises(ModelUnavailableError, match="image-capable"):
    -        await state.seal(None)  # type: ignore[arg-type]
    -
    -    assert opens == 0
    -    assert state.sealed is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_credentials_are_connection_scoped_and_refresh_keeps_revision(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(_connection(0, "first", token="one"))
    -    revision = store.add_connection(_connection(revision, "second", token="two"))
    -    first = store.credential_handle("first", "shared-account")
    -    second = store.credential_handle("second", "shared-account")
    -
    -    await first.refresh({"driver": "api_key", "access_token": "rotated"})
    -
    -    assert (await first.read())["access_token"] == "rotated"
    -    assert (await second.read())["access_token"] == "two"
    -    assert store.read_snapshot().revision == revision  # type: ignore[union-attr]
    -
    -    revision = store.disable_connection(
    -        DisableConnection(expected_revision=revision, connection_id="first")
    -    )
    -    await first.refresh({"driver": "api_key", "access_token": "draining"})
    -    assert (await first.read())["access_token"] == "draining"
    -    assert store.read_snapshot().revision == revision  # type: ignore[union-attr]
    -
    -    with pytest.raises(RevisionConflictError):
    -        store.disable_connection(
    -            DisableConnection(expected_revision=0, connection_id="second")
    -        )
    -
    -    database_mode = stat.S_IMODE(store.path.stat().st_mode)
    -    assert database_mode == 0o600
    -    backups = tuple(store.backup_dir.glob("*.sqlite3"))
    -    assert len(backups) >= 5
    -    assert all(stat.S_IMODE(path.stat().st_mode) == 0o600 for path in backups)
    -
    -
    -@pytest.mark.asyncio
    -async def test_credential_exclusive_is_cross_process_and_cancel_safe(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    _ = store.add_connection(_connection(0, "connection", token="one"))
    -    handle = store.credential_handle("connection", "shared-account")
    -    lock_path = store.path.with_name(f"{store.path.name}.credentials.lock")
    -    child = subprocess.Popen(
    -        [
    -            sys.executable,
    -            "-c",
    -            (
    -                "import fcntl, pathlib, sys; "
    -                "f=pathlib.Path(sys.argv[1]).open('a+'); "
    -                "fcntl.flock(f.fileno(), fcntl.LOCK_EX); "
    -                "print('locked', flush=True); input(); f.close()"
    -            ),
    -            str(lock_path),
    -        ],
    -        stdin=subprocess.PIPE,
    -        stdout=subprocess.PIPE,
    -        text=True,
    -    )
    -    assert child.stdout is not None and child.stdout.readline().strip() == "locked"
    -
    -    entered = asyncio.Event()
    -
    -    async def wait_for_lock() -> None:
    -        async with handle.exclusive():
    -            entered.set()
    -
    -    waiter = asyncio.create_task(wait_for_lock())
    -    await asyncio.sleep(0)
    -    await asyncio.sleep(0)
    -    assert not entered.is_set()
    -    waiter.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await waiter
    -
    -    assert child.stdin is not None
    -    child.stdin.write("\n")
    -    child.stdin.flush()
    -    assert child.wait(timeout=5) == 0
    -    child.stdin.close()
    -    child.stdout.close()
    -    async with handle.exclusive():
    -        assert True
    -
    -    read_only = ModelsStore(
    -        store.path,
    -        backup_dir=store.backup_dir,
    -        writable=False,
    -    ).credential_handle("connection", "shared-account")
    -    lock_path.unlink()
    -    with pytest.raises(PermissionError, match="read-only"):
    -        async with read_only.exclusive():
    -            pass
    -    assert not lock_path.exists()
    -
    -
    -def test_model_capabilities_sources_and_driver_config_round_trip(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(_connection(0, "connection", token="one"))
    -    capabilities = ModelCapabilities(
    -        context_window=1234,
    -        max_output_tokens=234,
    -        input_modalities=("text", "image"),
    -        supports_tool_calls=False,
    -        supports_parallel_tool_calls=False,
    -        supported_reasoning_efforts=("low", "high"),
    -    )
    -    sources = CapabilitySources(
    -        context_window="context-source",
    -        max_output_tokens="output-source",
    -        input_modalities="modalities-source",
    -        tool_calls="tool-source",
    -        parallel_tool_calls="parallel-source",
    -        reasoning_efforts="reasoning-source",
    -    )
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="chat",
    -            connection_id="connection",
    -            kind=ModelKind.CHAT,
    -            model="wire-chat",
    -            capabilities=capabilities,
    -            capability_sources=sources,
    -            default_reasoning_effort="high",
    -            driver_config={
    -                "use_responses_lite": True,
    -                "reasoning_summary": "auto",
    -                "nested": {"value": [1, 2]},
    -            },
    -        )
    -    )
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="embedding",
    -            connection_id="connection",
    -            kind=ModelKind.EMBEDDING,
    -            model="wire-embedding",
    -            capabilities=ModelCapabilities(
    -                embedding_dimensions=3,
    -                embedding_normalization="l2",
    -            ),
    -            capability_sources=CapabilitySources(
    -                embedding_dimensions="dimension-source",
    -                embedding_normalization="normalization-source",
    -            ),
    -            driver_config={"batch_size": 8},
    -        )
    -    )
    -
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None and snapshot.revision == revision
    -    chat = snapshot.models["chat"]
    -    assert chat.capabilities == capabilities
    -    assert chat.capability_sources == sources
    -    assert chat.default_reasoning_effort == "high"
    -    assert chat.driver_config["nested"]["value"] == (1, 2)  # type: ignore[index]
    -    embedding = snapshot.models["embedding"]
    -    assert embedding.capabilities.embedding_normalization == "l2"
    -    assert (
    -        embedding.capability_sources.embedding_normalization == "normalization-source"
    -    )
    -    assert snapshot.connections["connection"].driver_config == {}
    -    public_catalog = ModelsState(
    -        store,
    -        root_instance_token=object(),
    -    ).catalog_snapshot()
    -    assert "public-catalog-secret" not in repr(public_catalog)
    -    assert not hasattr(public_catalog.connections[0], "endpoint")
    -
    -    with pytest.raises(ValueError, match="finite"):
    -        store.add_model(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="invalid",
    -                connection_id="connection",
    -                kind=ModelKind.CHAT,
    -                model="invalid",
    -                capabilities=ModelCapabilities(),
    -                capability_sources=CapabilitySources(),
    -                driver_config={"bad": float("nan")},
    -            )
    -        )
    -
    -
    -def test_connection_legacy_provider_column_matches_driver_config(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(
    -        AddConnection(
    -            expected_revision=0,
    -            connection_id="connection",
    -            name="connection",
    -            driver_id="openai-compatible",
    -            endpoint="https://example.test/v1",
    -            auth_identity="account",
    -            credential={"driver": "api_key", "access_token": "secret"},
    -            driver_config={"catalog_provider_id": "deepseek"},
    -        )
    -    )
    -
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None and snapshot.revision == revision
    -    assert snapshot.connections["connection"].driver_config == {
    -        "catalog_provider_id": "deepseek"
    -    }
    -    with closing(sqlite3.connect(store.path)) as connection:
    -        row = connection.execute(
    -            "SELECT catalog_provider_id FROM model_connections WHERE id = 'connection'"
    -        ).fetchone()
    -    assert row == ("deepseek",)
    -    with pytest.raises(ValueError, match="outer whitespace"):
    -        store.add_connection(
    -            AddConnection(
    -                expected_revision=revision,
    -                connection_id="invalid",
    -                name="invalid",
    -                driver_id="openai-compatible",
    -                endpoint="https://example.test/v1",
    -                auth_identity="account",
    -                credential={"driver": "api_key", "access_token": "secret"},
    -                driver_config={"catalog_provider_id": " deepseek "},
    -            )
    -        )
    -
    -
    -def test_model_ids_are_unique_across_kinds_and_corruption_fails_loud(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(_connection(0, "connection", token="one"))
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="same",
    -            connection_id="connection",
    -            kind=ModelKind.CHAT,
    -            model="chat-wire",
    -            capabilities=ModelCapabilities(),
    -            capability_sources=CapabilitySources(),
    -        )
    -    )
    -    with pytest.raises(ValueError, match="already exists"):
    -        store.add_model(
    -            AddModel(
    -                expected_revision=revision,
    -                model_id="same",
    -                connection_id="connection",
    -                kind=ModelKind.EMBEDDING,
    -                model="embedding-wire",
    -                capabilities=ModelCapabilities(embedding_dimensions=3),
    -                capability_sources=CapabilitySources(),
    -            )
    -        )
    -
    -    with closing(sqlite3.connect(store.path)) as connection:
    -        connection.execute(
    -            "INSERT INTO embedding_models(id, connection_id, model, dimensions) "
    -            "VALUES ('same', 'connection', 'embedding-wire', 3)"
    -        )
    -        connection.commit()
    -    with pytest.raises(RuntimeError, match="duplicate model id across kinds"):
    -        store.read_snapshot()
    -
    -
    -def test_discovery_sync_is_one_revision_and_preserves_store_owned_id(
    -    tmp_path: Path,
    -) -> None:
    -    store = ModelsStore(
    -        tmp_path / "workspace" / "model-registry.sqlite3",
    -        backup_dir=tmp_path / "workspace" / "runtime" / "model-backups",
    -    )
    -    revision = store.add_connection(_connection(0, "connection", token="one"))
    -    first = DiscoveredModel(
    -        kind=ModelKind.CHAT,
    -        model="wire-model",
    -        capabilities=ModelCapabilities(context_window=100),
    -        capability_sources=CapabilitySources(context_window="provider"),
    -        driver_config={"profile": "first"},
    -    )
    -    revision = store.sync_models(revision, "connection", (first,))
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None and snapshot.revision == revision
    -    model_id = next(iter(snapshot.models))
    -    assert model_id == "discovered:10:connection4:chat10:wire-model"
    -
    -    updated = DiscoveredModel(
    -        kind=ModelKind.CHAT,
    -        model="wire-model",
    -        capabilities=ModelCapabilities(context_window=200),
    -        capability_sources=CapabilitySources(context_window="provider-refresh"),
    -        driver_config={"profile": "second"},
    -    )
    -    revision = store.sync_models(revision, "connection", (updated,))
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None and snapshot.revision == revision
    -    assert tuple(snapshot.models) == (model_id,)
    -    assert snapshot.models[model_id].capabilities.context_window == 200
    -    assert snapshot.models[model_id].driver_config == {"profile": "second"}
    -
    -    extra = DiscoveredModel(
    -        kind=ModelKind.CHAT,
    -        model="removed-wire",
    -        capabilities=ModelCapabilities(context_window=50),
    -        capability_sources=CapabilitySources(context_window="provider"),
    -    )
    -    revision = store.sync_models(revision, "connection", (updated, extra))
    -    removed_id = "discovered:10:connection4:chat12:removed-wire"
    -    assert store.read_snapshot().models[removed_id].enabled is True  # type: ignore[union-attr]
    -    revision = store.sync_models(revision, "connection", (updated,))
    -    assert store.read_snapshot().models[removed_id].enabled is False  # type: ignore[union-attr]
    -    backups_before = tuple(store.backup_dir.glob("*.sqlite3"))
    -    assert store.sync_models(revision, "connection", (updated,)) == revision
    -    assert tuple(store.backup_dir.glob("*.sqlite3")) == backups_before
    -
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="legacy",
    -            connection_id="connection",
    -            kind=ModelKind.CHAT,
    -            model="legacy-wire",
    -            capabilities=ModelCapabilities(context_window=50),
    -            capability_sources=CapabilitySources(context_window="legacy"),
    -            driver_config={"profile": "legacy"},
    -        )
    -    )
    -    with closing(sqlite3.connect(store.path)) as connection:
    -        connection.execute(
    -            "UPDATE model_definitions SET capabilities_json = NULL WHERE id = 'legacy'"
    -        )
    -        connection.commit()
    -    legacy = DiscoveredModel(
    -        kind=ModelKind.CHAT,
    -        model="legacy-wire",
    -        capabilities=ModelCapabilities(context_window=999),
    -        capability_sources=CapabilitySources(context_window="provider"),
    -        driver_config={"profile": "provider"},
    -    )
    -    revision = store.sync_models(revision, "connection", (updated, legacy))
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None
    -    assert snapshot.models["legacy"].discovery_owned is True
    -    assert snapshot.models["legacy"].capabilities.context_window == 999
    -    assert snapshot.models["legacy"].driver_config == {"profile": "provider"}
    -    revision = store.sync_models(revision, "connection", (updated,))
    -    assert store.read_snapshot().models["legacy"].enabled is False  # type: ignore[union-attr]
    -
    -    revision = store.add_model(
    -        AddModel(
    -            expected_revision=revision,
    -            model_id="manual",
    -            connection_id="connection",
    -            kind=ModelKind.CHAT,
    -            model="manual-wire",
    -            capabilities=ModelCapabilities(context_window=777),
    -            capability_sources=CapabilitySources(context_window="manual"),
    -            driver_config={"profile": "manual"},
    -        )
    -    )
    -    revision = store.sync_models(
    -        revision,
    -        "connection",
    -        (
    -            updated,
    -            DiscoveredModel(
    -                kind=ModelKind.CHAT,
    -                model="manual-wire",
    -                capabilities=ModelCapabilities(context_window=999),
    -                capability_sources=CapabilitySources(context_window="provider"),
    -                driver_config={"profile": "provider"},
    -            ),
    -        ),
    -    )
    -    snapshot = store.read_snapshot()
    -    assert snapshot is not None
    -    assert snapshot.models["manual"].capabilities.context_window == 777
    -    assert snapshot.models["manual"].driver_config == {"profile": "manual"}
    -
    -    with pytest.raises(ValueError, match="duplicate model"):
    -        store.sync_models(revision, "connection", (updated, updated))
    -    assert store.read_snapshot().revision == revision  # type: ignore[union-attr]
    -
    -    invalid_items = (
    -        (),
    -        (
    -            DiscoveredModel(
    -                kind="chat",  # type: ignore[arg-type]
    -                model="invalid-kind",
    -                capabilities=ModelCapabilities(),
    -                capability_sources=CapabilitySources(),
    -            ),
    -        ),
    -        (
    -            DiscoveredModel(
    -                kind=ModelKind.CHAT,
    -                model=" padded ",
    -                capabilities=ModelCapabilities(),
    -                capability_sources=CapabilitySources(),
    -            ),
    -        ),
    -        (
    -            DiscoveredModel(
    -                kind=ModelKind.CHAT,
    -                model="invalid-capabilities",
    -                capabilities={},  # type: ignore[arg-type]
    -                capability_sources=CapabilitySources(),
    -            ),
    -        ),
    -    )
    -    for invalid in invalid_items:
    -        with pytest.raises((TypeError, ValueError)):
    -            store.sync_models(revision, "connection", invalid)  # type: ignore[arg-type]
    -
    -    revision = store.disable_connection(
    -        DisableConnection(expected_revision=revision, connection_id="connection")
    -    )
    -    with pytest.raises(ValueError, match="disabled"):
    -        store.sync_models(revision, "connection", (updated,))
    diff --git a/tests/test_more_support_modules.py b/tests/test_more_support_modules.py
    deleted file mode 100644
    index 8dd82f8a0..000000000
    --- a/tests/test_more_support_modules.py
    +++ /dev/null
    @@ -1,152 +0,0 @@
    -from __future__ import annotations
    -
    -import runpy
    -import sys
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, cast
    -from unittest.mock import AsyncMock, MagicMock
    -
    -import pytest
    -
    -from bootstrap.app import AppRuntime
    -from bus.event_bus import EventBus
    -from infra.channels.group_filter import DefaultGroupFilter, strip_at_segments
    -
    -
    -@pytest.mark.asyncio
    -async def test_app_runtime_start_has_no_core_markdown_optimizer(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -) -> None:
    -    startup_order: list[str] = []
    -    plugin_manager = MagicMock()
    -    plugin_manager.bind_core_channel_definitions = AsyncMock(
    -        side_effect=lambda definitions: startup_order.append("bindings")
    -    )
    -    plugin_manager.run_runtime_services = AsyncMock()
    -    snapshot_store = MagicMock()
    -    plugin_manager.snapshot_store = snapshot_store
    -    core = SimpleNamespace(
    -        loop=SimpleNamespace(
    -            run=lambda: "loop-task",
    -            bind_plugin_rollout_fact_provider=MagicMock(),
    -        ),
    -        bus=SimpleNamespace(dispatch_outbound=lambda: "bus-task"),
    -        event_bus=EventBus(),
    -        tools=MagicMock(),
    -        push_tool=MagicMock(),
    -        session_manager=MagicMock(),
    -        channel_attachment_store=MagicMock(),
    -        presence=MagicMock(),
    -        plugin_manager=plugin_manager,
    -        bind_conversation_runtime=MagicMock(),
    -        start=AsyncMock(),
    -        stop=AsyncMock(),
    -    )
    -    monkeypatch.setattr(
    -        "bootstrap.app.build_core_runtime", lambda *args, **kwargs: core
    -    )
    -    channel_host = SimpleNamespace(
    -        start_all=AsyncMock(side_effect=lambda: startup_order.append("providers")),
    -        stop_all=AsyncMock(),
    -        bind_plugin_channels=MagicMock(),
    -        swap_plugin_channels=AsyncMock(),
    -        channels=(),
    -    )
    -    monkeypatch.setattr(
    -        "bootstrap.app.start_channels",
    -        AsyncMock(return_value=channel_host),
    -    )
    -    dashboard_calls: list[dict[str, object]] = []
    -
    -    def build_dashboard_server(**kwargs: object) -> SimpleNamespace:
    -        dashboard_calls.append(kwargs)
    -        return SimpleNamespace(should_exit=False, serve=AsyncMock(return_value=None))
    -
    -    monkeypatch.setattr(
    -        "bootstrap.app.build_dashboard_server",
    -        build_dashboard_server,
    -    )
    -
    -    app = AppRuntime(
    -        config=cast(
    -            Any,
    -            SimpleNamespace(
    -                app_server=SimpleNamespace(enabled=False),
    -                channels=SimpleNamespace(chat=SimpleNamespace(enabled=False)),
    -                mobile_realtime=SimpleNamespace(enabled=False),
    -            ),
    -        ),
    -        workspace=tmp_path,
    -    )
    -    await app.start()
    -
    -    assert len(dashboard_calls) == 1
    -    assert "manual_memory_optimizer" not in dashboard_calls[0]
    -    assert "memory_store" not in dashboard_calls[0]
    -    assert startup_order == ["bindings", "providers"]
    -    await app.shutdown()
    -
    -
    -@pytest.mark.asyncio
    -async def test_group_filter_paths() -> None:
    -    group = SimpleNamespace(group_id="1", allow_from=["42"], require_at=True)
    -    event = SimpleNamespace(user_id="42", raw_message="[CQ:at,qq=10001] hi")
    -
    -    assert (
    -        await DefaultGroupFilter("10001").should_process(event, cast(Any, group))
    -        is True
    -    )
    -    assert strip_at_segments("x [CQ:at,qq=10001] y") == "x  y".strip()
    -
    -    bad_user = SimpleNamespace(user_id="9", raw_message="hi")
    -    assert (
    -        await DefaultGroupFilter("10001").should_process(bad_user, cast(Any, group))
    -        is False
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_bootstrap_trigger_and_entrypoints_cover_paths(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -) -> None:
    -    from agent.migrations import MigrationOutcome
    -
    -    supervisor_calls: list[tuple[Path, Path]] = []
    -
    -    def _fake_supervisor(
    -        *,
    -        config_path: Path,
    -        workspace: Path,
    -        readiness_timeout_s: float = 15.0,
    -    ) -> int:
    -        supervisor_calls.append((config_path, workspace))
    -        return 0
    -
    -    def _fake_migration(config_path: Path, workspace: Path) -> MigrationOutcome:
    -        return MigrationOutcome(state="current")
    -
    -    monkeypatch.setattr("agent.supervisor.run_supervisor", _fake_supervisor)
    -    monkeypatch.setattr("agent.migrations.migrate_installation", _fake_migration)
    -    monkeypatch.setattr("pathlib.Path.exists", lambda self: False)
    -    monkeypatch.setattr(
    -        sys,
    -        "argv",
    -        ["main.py", "--config", "missing.json", "--workspace", str(tmp_path)],
    -    )
    -    with pytest.raises(SystemExit) as exc:
    -        runpy.run_module("main", run_name="__main__")
    -    assert exc.value.code == 0
    -    assert supervisor_calls == [(Path("missing.json"), tmp_path)]
    -
    -    monkeypatch.setattr("pathlib.Path.exists", lambda self: True)
    -    supervisor_calls.clear()
    -    monkeypatch.setattr(
    -        sys,
    -        "argv",
    -        ["main.py", "--workspace", str(tmp_path)],
    -    )
    -    with pytest.raises(SystemExit) as exc:
    -        runpy.run_module("main", run_name="__main__")
    -    assert exc.value.code == 0
    -    assert supervisor_calls == [(Path("config.toml"), tmp_path)]
    diff --git a/tests/test_native_telegram_qq.py b/tests/test_native_telegram_qq.py
    deleted file mode 100644
    index 2c389369e..000000000
    --- a/tests/test_native_telegram_qq.py
    +++ /dev/null
    @@ -1,637 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import base64
    -import hashlib
    -from datetime import datetime, timezone
    -from types import SimpleNamespace
    -from typing import Any, Mapping
    -from unittest.mock import AsyncMock
    -
    -import pytest
    -
    -from agent.plugin_composition.channels import (
    -    AttachmentKind,
    -    AttachmentReadLease,
    -    AttachmentRef,
    -    ChannelAttachmentImportPort,
    -    ChannelAttachmentReadPort,
    -    ChannelFactoryContext,
    -    ChannelIngressPort,
    -    ChannelRuntimePorts,
    -    ChannelReady,
    -    CredentialRef,
    -    DeliveryStatus,
    -    RawInbound,
    -    ProviderDeliveryRequest,
    -    ProviderClient,
    -)
    -
    -from tests.test_channel_clients import (
    -    _Bus,
    -    _SessionManager,
    -    _import_qq_channel,
    -    _import_telegram_channel,
    -)
    -
    -
    -class _ProviderFactory:
    -    async def create(
    -        self,
    -        credentials: Mapping[str, CredentialRef],
    -    ) -> ProviderClient:
    -        raise AssertionError("native channel must reuse the existing provider owner")
    -
    -    async def aclose(self) -> None:
    -        raise AssertionError(
    -            "native channel must not close the existing provider owner"
    -        )
    -
    -
    -class _Lease:
    -    def __init__(self, ref: AttachmentRef, data: bytes, events: list[str]) -> None:
    -        self.ref = ref
    -        self._data = data
    -        self._events = events
    -
    -    async def read_bytes(self, *, max_bytes: int) -> bytes:
    -        assert max_bytes >= len(self._data)
    -        self._events.append(f"read:{self.ref.artifact_id}")
    -        return self._data
    -
    -    async def aclose(self) -> None:
    -        self._events.append(f"close:{self.ref.artifact_id}")
    -
    -
    -class _ReadPort:
    -    def __init__(self, blobs: dict[str, bytes], events: list[str]) -> None:
    -        self._blobs = blobs
    -        self._events = events
    -
    -    async def acquire(self, ref: AttachmentRef) -> _Lease:
    -        self._events.append(f"acquire:{ref.artifact_id}")
    -        return _Lease(ref, self._blobs[ref.artifact_id], self._events)
    -
    -
    -def _context(
    -    binding_token: str,
    -    read_port: ChannelAttachmentReadPort,
    -    *,
    -    ingress: ChannelIngressPort | None = None,
    -    attachment_import: ChannelAttachmentImportPort | None = None,
    -) -> ChannelFactoryContext:
    -    return ChannelFactoryContext(
    -        snapshot_id="snapshot-1",
    -        generation_id="generation-1",
    -        binding_token=binding_token,
    -        config={},
    -        credentials={},
    -        provider_client_factory=_ProviderFactory(),
    -        ingress=ingress,
    -        identity=None,
    -        attachment_import=attachment_import,
    -        attachment_read=read_port,
    -    )
    -
    -
    -class _Ingress:
    -    def __init__(self) -> None:
    -        self.messages: list[RawInbound] = []
    -
    -    async def admit(self, raw: RawInbound) -> bool:
    -        self.messages.append(raw)
    -        return True
    -
    -
    -class _ImportPort:
    -    def __init__(self) -> None:
    -        self.calls: list[tuple[bytes, AttachmentKind, str | None, str | None]] = []
    -        self._counter = 0
    -
    -    async def import_bytes(
    -        self,
    -        data: bytes,
    -        *,
    -        kind: AttachmentKind,
    -        filename: str | None,
    -        media_type: str | None,
    -    ) -> AttachmentRef:
    -        self.calls.append((data, kind, filename, media_type))
    -        self._counter += 1
    -        return _ref(
    -            f"inbound-{self._counter}",
    -            kind,
    -            filename or f"inbound-{self._counter}",
    -            media_type or "application/octet-stream",
    -            data,
    -        )
    -
    -
    -class _FailingReadPort:
    -    async def acquire(self, ref: AttachmentRef) -> AttachmentReadLease:
    -        raise RuntimeError("read rejected")
    -
    -
    -def _runtime_context(
    -    binding_token: str,
    -    ingress: _Ingress,
    -    attachment_import: _ImportPort,
    -) -> ChannelFactoryContext:
    -    return _context(
    -        binding_token,
    -        _ReadPort({}, []),
    -        ingress=ingress,
    -        attachment_import=attachment_import,
    -    )
    -
    -
    -def _ref(
    -    artifact_id: str,
    -    kind: AttachmentKind,
    -    filename: str,
    -    media_type: str,
    -    data: bytes,
    -) -> AttachmentRef:
    -    return AttachmentRef(
    -        artifact_id=artifact_id,
    -        kind=kind,
    -        filename=filename,
    -        media_type=media_type,
    -        size_bytes=len(data),
    -        sha256=hashlib.sha256(data).hexdigest(),
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_v3_adapter_delivers_in_order_from_exact_leases(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = mod.TelegramChannel("token", _Bus(), _SessionManager(tmp_path))
    -    channel._telegram_outbound_limiter = mod.TelegramOutboundLimiter(
    -        send_interval_s=0.0,
    -        edit_interval_s=0.0,
    -        typing_interval_s=0.0,
    -        global_interval_s=0.0,
    -        retry_padding_s=0.0,
    -    )
    -    channel._app.initialize = AsyncMock()
    -    channel._app.start = AsyncMock()
    -    events: list[str] = []
    -    photo = b"photo"
    -    document = b"document"
    -    image_ref = _ref("image-1", AttachmentKind.IMAGE, "image.png", "image/png", photo)
    -    file_ref = _ref(
    -        "file-1", AttachmentKind.FILE, "report.pdf", "application/pdf", document
    -    )
    -    read_port = _ReadPort(
    -        {image_ref.artifact_id: photo, file_ref.artifact_id: document},
    -        events,
    -    )
    -    context = _context("telegram-binding", read_port)
    -
    -    async def send_message(**kwargs: Any) -> None:
    -        events.append(f"text:{kwargs['text']}")
    -
    -    async def send_photo(**kwargs: Any) -> None:
    -        events.append(f"photo:{kwargs['photo'].getvalue().decode()}")
    -
    -    async def send_document(**kwargs: Any) -> None:
    -        events.append(
    -            f"file:{kwargs['filename']}:{kwargs['document'].getvalue().decode()}"
    -        )
    -
    -    channel._app.bot.send_message = send_message
    -    channel._app.bot.send_photo = send_photo
    -    channel._app.bot.send_document = send_document
    -    adapter = channel.build_v3_adapter(context)
    -
    -    assert await adapter.start() == ChannelReady("telegram-binding")
    -    channel._app.initialize.assert_not_awaited()
    -    channel._app.start.assert_not_awaited()
    -    receipt = await adapter.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="telegram-binding",
    -            delivery_id="delivery-1",
    -            recipient="123",
    -            body="hello",
    -            attachments=(image_ref, file_ref),
    -        )
    -    )
    -
    -    assert receipt.status is DeliveryStatus.DELIVERED
    -    assert events == [
    -        "text:hello",
    -        "acquire:image-1",
    -        "read:image-1",
    -        "close:image-1",
    -        "photo:photo",
    -        "acquire:file-1",
    -        "read:file-1",
    -        "close:file-1",
    -        "file:report.pdf:document",
    -    ]
    -    assert (await adapter.stop()).resources_closed is True
    -
    -    with pytest.raises(RuntimeError, match="binding token"):
    -        await adapter.deliver(
    -            ProviderDeliveryRequest(
    -                binding_token="wrong-binding",
    -                delivery_id="wrong-binding",
    -                recipient="123",
    -                body="must not send",
    -            )
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_v3_adapter_maps_pre_and_post_provider_failures(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = mod.TelegramChannel("token", _Bus(), _SessionManager(tmp_path))
    -    events: list[str] = []
    -    data = b"payload"
    -    ref = _ref("file-1", AttachmentKind.FILE, "a.txt", "text/plain", data)
    -    adapter = channel.build_v3_adapter(
    -        _context("telegram-binding", _ReadPort({ref.artifact_id: data}, events))
    -    )
    -
    -    rejected = await adapter.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="telegram-binding",
    -            delivery_id="invalid-recipient",
    -            recipient="not-a-chat",
    -            body="hello",
    -        )
    -    )
    -    assert rejected.status is DeliveryStatus.REJECTED
    -
    -    async def fail_send_message(**_kwargs: Any) -> None:
    -        raise RuntimeError("provider failed")
    -
    -    channel._app.bot.send_message = fail_send_message
    -    unknown = await adapter.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="telegram-binding",
    -            delivery_id="provider-failure",
    -            recipient="123",
    -            body="hello",
    -        )
    -    )
    -    assert unknown.status is DeliveryStatus.UNKNOWN
    -
    -    no_provider = channel.build_v3_adapter(
    -        _context("telegram-binding-2", _FailingReadPort())
    -    )
    -    rejected_attachment = await no_provider.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="telegram-binding-2",
    -            delivery_id="read-failure",
    -            recipient="123",
    -            body="",
    -            attachments=(ref,),
    -        )
    -    )
    -    assert rejected_attachment.status is DeliveryStatus.REJECTED
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_v3_adapter_delivers_group_text_and_binary_payloads(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_qq_channel(monkeypatch)
    -    channel = mod.QQChannel(
    -        "42",
    -        _Bus(),
    -        _SessionManager(tmp_path),
    -        http_requester=SimpleNamespace(),
    -    )
    -    events: list[tuple[object, ...]] = []
    -
    -    class _Api:
    -        async def send_group_text(self, group_id: int, body: str) -> None:
    -            events.append(("text", group_id, body))
    -
    -        async def send_group_image(self, group_id: int, uri: str) -> None:
    -            events.append(
    -                ("image", group_id, base64.b64decode(uri.removeprefix("base64://")))
    -            )
    -
    -        async def send_group_file(self, group_id: int, uri: str, filename: str) -> None:
    -            events.append(
    -                (
    -                    "file",
    -                    group_id,
    -                    filename,
    -                    base64.b64decode(uri.removeprefix("base64://")),
    -                )
    -            )
    -
    -    channel._api = _Api()
    -
    -    async def run(coro: Any) -> object:
    -        return await coro
    -
    -    channel._run_on_bot_loop = AsyncMock(side_effect=run)
    -    image = b"qq-image"
    -    document = b"qq-document"
    -    image_ref = _ref("image-1", AttachmentKind.IMAGE, "image.jpg", "image/jpeg", image)
    -    file_ref = _ref("file-1", AttachmentKind.FILE, "report.txt", "text/plain", document)
    -    events_lease: list[str] = []
    -    adapter = channel.build_v3_adapter(
    -        _context(
    -            "qq-binding",
    -            _ReadPort(
    -                {image_ref.artifact_id: image, file_ref.artifact_id: document},
    -                events_lease,
    -            ),
    -        )
    -    )
    -
    -    receipt = await adapter.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="qq-binding",
    -            delivery_id="delivery-1",
    -            recipient="gqq:100",
    -            body="hello",
    -            attachments=(image_ref, file_ref),
    -        )
    -    )
    -
    -    assert receipt.status is DeliveryStatus.DELIVERED
    -    assert events == [
    -        ("text", 100, "hello"),
    -        ("image", 100, image),
    -        ("file", 100, "report.txt", document),
    -    ]
    -    assert events_lease == [
    -        "acquire:image-1",
    -        "read:image-1",
    -        "close:image-1",
    -        "acquire:file-1",
    -        "read:file-1",
    -        "close:file-1",
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_v3_adapter_rejects_invalid_recipient_before_provider(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_qq_channel(monkeypatch)
    -    channel = mod.QQChannel(
    -        "42",
    -        _Bus(),
    -        _SessionManager(tmp_path),
    -        http_requester=SimpleNamespace(),
    -    )
    -    channel._api = SimpleNamespace()
    -    adapter = channel.build_v3_adapter(_context("qq-binding", _ReadPort({}, [])))
    -
    -    receipt = await adapter.deliver(
    -        ProviderDeliveryRequest(
    -            binding_token="qq-binding",
    -            delivery_id="invalid-recipient",
    -            recipient="gqq:not-a-number",
    -            body="hello",
    -        )
    -    )
    -    assert receipt.status is DeliveryStatus.REJECTED
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_v3_inbound_waits_for_open_and_imports_reply_media(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = mod.TelegramChannel("token", _Bus(), _SessionManager(tmp_path))
    -    ingress = _Ingress()
    -    attachment_import = _ImportPort()
    -    context = _runtime_context("telegram-inbound", ingress, attachment_import)
    -    adapter = channel.build_v3_adapter(context)
    -    adapter.attach_runtime(
    -        ChannelRuntimePorts(
    -            snapshot_id=context.snapshot_id,
    -            generation_id=context.generation_id,
    -            binding_token=context.binding_token,
    -            ingress=context.ingress,
    -            identity=context.identity,
    -            attachment_import=context.attachment_import,
    -        )
    -    )
    -
    -    reply = SimpleNamespace(
    -        text="原消息",
    -        caption="",
    -        photo=[SimpleNamespace(file_id="reply-photo")],
    -        document=SimpleNamespace(
    -            file_id="reply-file", file_name="note.txt", mime_type="text/plain"
    -        ),
    -        from_user=SimpleNamespace(id=8, username="bob"),
    -        message_id=8,
    -    )
    -    message = SimpleNamespace(
    -        message_id=9,
    -        text="你好",
    -        caption="",
    -        photo=None,
    -        document=None,
    -        reply_to_message=reply,
    -        date=datetime(2026, 8, 20, tzinfo=timezone.utc),
    -    )
    -    update = SimpleNamespace(
    -        effective_message=message,
    -        effective_chat=SimpleNamespace(id=123),
    -        effective_user=SimpleNamespace(id=7, username="alice"),
    -    )
    -
    -    class _File:
    -        def __init__(self, payload: bytes) -> None:
    -            self.payload = payload
    -
    -        async def download_as_bytearray(self) -> bytearray:
    -            return bytearray(self.payload)
    -
    -    channel._app.bot.get_file = AsyncMock(
    -        side_effect=[_File(b"reply-image"), _File(b"reply-file")]
    -    )
    -    handler = asyncio.create_task(
    -        channel._on_message(update, SimpleNamespace(bot=channel.bot))
    -    )
    -    await asyncio.sleep(0)
    -    assert ingress.messages == []
    -
    -    adapter.open_admission()
    -    await handler
    -
    -    assert len(ingress.messages) == 1
    -    raw = ingress.messages[0]
    -    assert raw.message_id == "9"
    -    assert raw.provider_identity == "7"
    -    assert raw.recipient == "123"
    -    assert "原消息" in raw.message.content
    -    assert "你好" in raw.message.content
    -    assert [ref.kind for ref in raw.message.attachments] == [
    -        AttachmentKind.IMAGE,
    -        AttachmentKind.FILE,
    -    ]
    -    assert [call[0] for call in attachment_import.calls] == [
    -        b"reply-image",
    -        b"reply-file",
    -    ]
    -    adapter.close_admission()
    -    assert (await adapter.stop()).resources_closed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_qq_v3_inbound_preserves_identity_and_imports_images(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_qq_channel(monkeypatch)
    -
    -    class _Response:
    -        status_code = 200
    -        headers = {"content-type": "image/png"}
    -
    -        async def aiter_bytes(self, *, chunk_size: int):
    -            _ = chunk_size
    -            yield b"qq-image"
    -
    -    from contextlib import asynccontextmanager
    -
    -    class _Requester:
    -        @asynccontextmanager
    -        async def stream(self, *_args: Any, **_kwargs: Any):
    -            yield _Response()
    -
    -    channel = mod.QQChannel(
    -        "42",
    -        _Bus(),
    -        _SessionManager(tmp_path),
    -        http_requester=_Requester(),
    -    )
    -    ingress = _Ingress()
    -    attachment_import = _ImportPort()
    -    context = _runtime_context("qq-inbound", ingress, attachment_import)
    -    adapter = channel.build_v3_adapter(context)
    -    adapter.attach_runtime(
    -        ChannelRuntimePorts(
    -            snapshot_id=context.snapshot_id,
    -            generation_id=context.generation_id,
    -            binding_token=context.binding_token,
    -            ingress=context.ingress,
    -            identity=context.identity,
    -            attachment_import=context.attachment_import,
    -        )
    -    )
    -    event = SimpleNamespace(message_id="qq-77", time=1724140800)
    -    pending = asyncio.create_task(
    -        channel._handle_private(
    -            "10001",
    -            "hello",
    -            ["http://qq.invalid/a.png"],
    -            message_id="qq-77",
    -            event=event,
    -        )
    -    )
    -    await asyncio.sleep(0)
    -    assert ingress.messages == []
    -    adapter.open_admission()
    -    await pending
    -
    -    assert len(ingress.messages) == 1
    -    raw = ingress.messages[0]
    -    assert raw.message_id == "qq-77"
    -    assert raw.provider_identity == "10001"
    -    assert raw.recipient == "10001"
    -    assert raw.message.content == "hello"
    -    assert raw.message.attachments[0].kind is AttachmentKind.IMAGE
    -    assert attachment_import.calls[0][0] == b"qq-image"
    -    adapter.close_admission()
    -    assert (await adapter.stop()).resources_closed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_telegram_v3_inflight_callback_cannot_cross_binding_after_reload(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path,
    -) -> None:
    -    mod = _import_telegram_channel(monkeypatch)
    -    channel = mod.TelegramChannel("token", _Bus(), _SessionManager(tmp_path))
    -    ingress_formal = _Ingress()
    -    import_formal = _ImportPort()
    -    formal = _runtime_context("telegram-formal", ingress_formal, import_formal)
    -    adapter = channel.build_v3_adapter(formal)
    -    adapter.attach_runtime(
    -        ChannelRuntimePorts(
    -            snapshot_id=formal.snapshot_id,
    -            generation_id=formal.generation_id,
    -            binding_token=formal.binding_token,
    -            ingress=formal.ingress,
    -            identity=formal.identity,
    -            attachment_import=formal.attachment_import,
    -        )
    -    )
    -    adapter.open_admission()
    -
    -    release_download = asyncio.Event()
    -
    -    class _File:
    -        async def download_as_bytearray(self) -> bytearray:
    -            await release_download.wait()
    -            return bytearray(b"old-binding")
    -
    -    channel._app.bot.get_file = AsyncMock(return_value=_File())
    -    message = SimpleNamespace(
    -        message_id=21,
    -        text="",
    -        caption="photo",
    -        photo=[SimpleNamespace(file_id="old-photo")],
    -        document=None,
    -        reply_to_message=None,
    -        date=datetime.now(timezone.utc),
    -    )
    -    update = SimpleNamespace(
    -        effective_message=message,
    -        effective_chat=SimpleNamespace(id=321),
    -        effective_user=SimpleNamespace(id=7, username="alice"),
    -    )
    -    old_callback = asyncio.create_task(
    -        channel._on_photo(update, SimpleNamespace(bot=channel.bot))
    -    )
    -    await asyncio.sleep(0)
    -
    -    adapter.close_admission()
    -    ingress_candidate = _Ingress()
    -    import_candidate = _ImportPort()
    -    candidate = _runtime_context(
    -        "telegram-candidate", ingress_candidate, import_candidate
    -    )
    -    candidate_adapter = channel.build_v3_adapter(candidate)
    -    candidate_adapter.attach_runtime(
    -        ChannelRuntimePorts(
    -            snapshot_id=candidate.snapshot_id,
    -            generation_id=candidate.generation_id,
    -            binding_token=candidate.binding_token,
    -            ingress=candidate.ingress,
    -            identity=candidate.identity,
    -            attachment_import=candidate.attachment_import,
    -        )
    -    )
    -    candidate_adapter.open_admission()
    -    release_download.set()
    -    with pytest.raises(RuntimeError, match="admission 已关闭"):
    -        await old_callback
    -    assert ingress_formal.messages == []
    -    assert ingress_candidate.messages == []
    -    assert import_formal.calls == []
    -    assert import_candidate.calls == []
    -    candidate_adapter.close_admission()
    -    assert (await candidate_adapter.stop()).resources_closed is True
    diff --git a/tests/test_openai_compatible_model_plugin.py b/tests/test_openai_compatible_model_plugin.py
    deleted file mode 100644
    index b2927e1eb..000000000
    --- a/tests/test_openai_compatible_model_plugin.py
    +++ /dev/null
    @@ -1,1010 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import ast
    -import gzip
    -import json
    -import os
    -import shutil
    -import subprocess
    -import sys
    -import threading
    -import time
    -from collections.abc import AsyncIterator, Mapping
    -from contextlib import asynccontextmanager, contextmanager
    -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    -from pathlib import Path
    -from typing import Any, Iterator
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    AddModel,
    -    AuthenticationError,
    -    BoundModelDescriptor,
    -    CapabilitySources,
    -    CHAT_MODELS,
    -    ContextLengthError,
    -    DriverConnectionDescriptor,
    -    EMBEDDINGS,
    -    EmbeddingSpaceDescriptor,
    -    ModelCapabilities,
    -    ModelAvailability,
    -    MODEL_CATALOG,
    -    MODEL_SETTINGS,
    -    InvalidRequestError,
    -    ModelKind,
    -    ModelRequest,
    -    ModelRole,
    -    RateLimitError,
    -    QuotaError,
    -    SetDefaultModel,
    -    SyncModels,
    -    TransportError,
    -    UsageCoverage,
    -)
    -from agent.plugins.install import (
    -    finalize_uninstall_plugin,
    -    install_git_plugin,
    -    set_installed_plugin_enabled,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from bus.event_bus import EventBus
    -from plugins.openai_compatible.driver import definition
    -
    -
    -class _Credential:
    -    def __init__(self, token: str = "secret") -> None:
    -        self.connection_id = "connection-1"
    -        self.auth_identity = "account-1"
    -        self.token = token
    -
    -    async def read(self) -> Mapping[str, str]:
    -        return {"driver": "api_key", "access_token": self.token}
    -
    -    async def refresh(self, payload: Mapping[str, str]) -> None:
    -        self.token = payload["access_token"]
    -
    -    @asynccontextmanager
    -    async def exclusive(self) -> AsyncIterator[None]:
    -        yield
    -
    -
    -class _Server(ThreadingHTTPServer):
    -    daemon_threads = True
    -
    -    def __init__(
    -        self,
    -        address: tuple[str, int],
    -        *,
    -        models_status: int = 200,
    -        model_ids: list[str] | None = None,
    -        gzip_models: bool = False,
    -    ) -> None:
    -        super().__init__(address, _Handler)
    -        self.requests: list[dict[str, Any]] = []
    -        self.slow_started = threading.Event()
    -        self.release_plain_done = threading.Event()
    -        self.models_status = models_status
    -        self.model_ids = model_ids
    -        self.gzip_models = gzip_models
    -
    -
    -class _Handler(BaseHTTPRequestHandler):
    -    server: _Server
    -
    -    def log_message(self, format: str, *args: object) -> None:
    -        _ = format, args
    -
    -    def do_GET(self) -> None:
    -        self.server.requests.append(
    -            {"path": self.path, "authorization": self.headers.get("Authorization")}
    -        )
    -        if self.path == "/v1/models":
    -            self._json(
    -                self.server.models_status,
    -                (
    -                    {
    -                        "object": "list",
    -                        "data": [
    -                            {"id": model_id}
    -                            for model_id in (self.server.model_ids or ["chat-a"])
    -                        ],
    -                    }
    -                    if self.server.models_status == 200
    -                    else {"error": {"message": "catalog unavailable"}}
    -                ),
    -                gzip_response=self.server.gzip_models,
    -            )
    -            return
    -        self._json(404, {"error": {"message": "missing"}})
    -
    -    def do_POST(self) -> None:
    -        length = int(self.headers.get("Content-Length") or 0)
    -        body = json.loads(self.rfile.read(length) or b"{}")
    -        self.server.requests.append(
    -            {
    -                "path": self.path,
    -                "authorization": self.headers.get("Authorization"),
    -                "body": body,
    -            }
    -        )
    -        if self.path == "/v1/embeddings":
    -            inputs = body.get("input") if isinstance(body.get("input"), list) else []
    -            vectors = (
    -                ([1.0, 0.0, 0.0], [0.0, 1.0, 0.0])
    -                if len(inputs) == 2
    -                else tuple(
    -                    [float(value.removeprefix("item-")), 0.0, 0.0]
    -                    if isinstance(value, str) and value.startswith("item-")
    -                    else [1.0, 0.0, 0.0]
    -                    for value in inputs
    -                )
    -            )
    -            self._json(
    -                200,
    -                {
    -                    "data": [
    -                        {"index": index, "embedding": vector}
    -                        for index, vector in reversed(tuple(enumerate(vectors)))
    -                    ],
    -                    "usage": {"prompt_tokens": 4},
    -                },
    -            )
    -            return
    -        if self.path != "/v1/chat/completions":
    -            self._json(404, {"error": {"message": "missing"}})
    -            return
    -        model = body.get("model")
    -        if model == "context-error":
    -            self._json(400, {"error": {"message": "maximum context length exceeded"}})
    -            return
    -        if model == "rate-error":
    -            self._json(429, {"error": {"message": "rate limited"}})
    -            return
    -        if model == "quota-error":
    -            self._json(429, {"error": {"message": "insufficient quota"}})
    -            return
    -        if model == "auth-error":
    -            self._json(401, {"error": {"message": "invalid token"}})
    -            return
    -        if model == "echo-secret-error":
    -            self._json(
    -                401,
    -                {"error": {"message": f"rejected {self.headers.get('Authorization')}"}},
    -            )
    -            return
    -        if model == "invalid-error":
    -            self._json(400, {"error": {"message": "unknown model"}})
    -            return
    -        if model == "slow-model":
    -            self.server.slow_started.set()
    -            time.sleep(2)
    -            self._json(200, _text_response("late"))
    -            return
    -        if model == "think-tags" and not body.get("stream"):
    -            self._json(200, _text_response("checked tagsanswer"))
    -            return
    -        if body.get("stream"):
    -            self.send_response(200)
    -            self.send_header("Content-Type", "text/event-stream")
    -            self.end_headers()
    -            if model == "think-tags":
    -                chunks = (
    -                    {"choices": [{"delta": {"content": "checked "}}]},
    -                    {"choices": [{"delta": {"content": "tagsanswer"}}]},
    -                )
    -            elif model == "think-tags-native-late":
    -                chunks = (
    -                    {
    -                        "choices": [
    -                            {"delta": {"content": "legacyanswer"}}
    -                        ]
    -                    },
    -                    {"choices": [{"delta": {"reasoning_content": "native"}}]},
    -                )
    -            elif model == "plain-stream":
    -                chunks = (
    -                    {"choices": [{"delta": {"content": "hello "}}]},
    -                    {"choices": [{"delta": {"content": "world"}}]},
    -                )
    -            else:
    -                chunks = (
    -                    {"choices": [{"delta": {"reasoning_content": "why "}}]},
    -                    {"choices": [{"delta": {"reasoning": "because "}}]},
    -                    {"choices": [{"delta": {"content": "hello "}}]},
    -                    {
    -                        "choices": [
    -                            {
    -                                "delta": {
    -                                    "tool_calls": [
    -                                        {
    -                                            "index": 0,
    -                                            "id": "call-1",
    -                                            "function": {
    -                                                "name": "search",
    -                                                "arguments": '{"q":',
    -                                            },
    -                                        }
    -                                    ]
    -                                }
    -                            }
    -                        ]
    -                    },
    -                    {
    -                        "choices": [
    -                            {
    -                                "delta": {
    -                                    "tool_calls": [
    -                                        {"index": 0, "function": {"arguments": '"hi"}'}}
    -                                    ]
    -                                },
    -                                "finish_reason": "tool_calls",
    -                            }
    -                        ]
    -                    },
    -                    {
    -                        "choices": [],
    -                        "usage": {
    -                            "prompt_tokens": 10,
    -                            "completion_tokens": 4,
    -                            "prompt_tokens_details": {"cached_tokens": 3},
    -                        },
    -                    },
    -                )
    -            for index, chunk in enumerate(chunks):
    -                self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
    -                self.wfile.flush()
    -                if model == "plain-stream" and index == 0:
    -                    self.server.release_plain_done.wait(timeout=2)
    -            if model != "truncated-stream":
    -                self.wfile.write(b"data: [DONE]\n\n")
    -            self.wfile.flush()
    -            return
    -        reasoning_field = (
    -            "reasoning" if model == "reasoning-alias" else "reasoning_content"
    -        )
    -        self._json(
    -            200,
    -            {
    -                "choices": [
    -                    {
    -                        "message": {
    -                            "content": None,
    -                            reasoning_field: "checked",
    -                            "tool_calls": [
    -                                {
    -                                    "id": "call-2",
    -                                    "function": {
    -                                        "name": "lookup",
    -                                        "arguments": '{"id": 7}',
    -                                    },
    -                                }
    -                            ],
    -                        },
    -                        "finish_reason": "tool_calls",
    -                    }
    -                ],
    -                "usage": {
    -                    "prompt_tokens": 8,
    -                    "completion_tokens": 2,
    -                    "completion_tokens_details": {"reasoning_tokens": 1},
    -                },
    -            },
    -        )
    -
    -    def _json(
    -        self,
    -        status: int,
    -        payload: Mapping[str, Any],
    -        *,
    -        gzip_response: bool = False,
    -    ) -> None:
    -        encoded = json.dumps(payload).encode()
    -        if gzip_response:
    -            encoded = gzip.compress(encoded)
    -        self.send_response(status)
    -        self.send_header("Content-Type", "application/json")
    -        if gzip_response:
    -            self.send_header("Content-Encoding", "gzip")
    -        self.send_header("Content-Length", str(len(encoded)))
    -        self.end_headers()
    -        try:
    -            self.wfile.write(encoded)
    -        except BrokenPipeError:
    -            pass
    -
    -
    -def _text_response(content: str) -> dict[str, Any]:
    -    return {
    -        "choices": [{"message": {"content": content}, "finish_reason": "stop"}],
    -        "usage": {"prompt_tokens": 1, "completion_tokens": 1},
    -    }
    -
    -
    -@contextmanager
    -def _provider(
    -    *,
    -    models_status: int = 200,
    -    model_ids: list[str] | None = None,
    -    gzip_models: bool = False,
    -) -> Iterator[tuple[_Server, str]]:
    -    server = _Server(
    -        ("127.0.0.1", 0),
    -        models_status=models_status,
    -        model_ids=model_ids,
    -        gzip_models=gzip_models,
    -    )
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    try:
    -        yield server, f"http://127.0.0.1:{server.server_port}/v1"
    -    finally:
    -        server.shutdown()
    -        server.server_close()
    -        thread.join(timeout=2)
    -
    -
    -def _connection(endpoint: str) -> DriverConnectionDescriptor:
    -    return DriverConnectionDescriptor(
    -        connection_id="connection-1",
    -        name="Test",
    -        driver_id="openai-compatible",
    -        endpoint=endpoint,
    -        auth_identity="account-1",
    -        config={"format_version": 1, "max_retries": 0},
    -    )
    -
    -
    -def _chat_descriptor(model: str = "chat-a") -> BoundModelDescriptor:
    -    return BoundModelDescriptor(
    -        binding_id=f"binding-{model}",
    -        plugin_snapshot_id="snapshot-1",
    -        model_revision=3,
    -        model_id=f"model-{model}",
    -        connection_id="connection-1",
    -        driver_id="openai-compatible",
    -        driver_contract_version="1",
    -        auth_identity="account-1",
    -        model=model,
    -        role=ModelRole.DEFAULT,
    -        reasoning_effort="high",
    -        capabilities=ModelCapabilities(context_window=8192, supports_tool_calls=True),
    -        capability_sources=CapabilitySources(context_window="test"),
    -        capability_digest="digest-chat",
    -    )
    -
    -
    -def _embedding_descriptor() -> EmbeddingSpaceDescriptor:
    -    return EmbeddingSpaceDescriptor(
    -        plugin_snapshot_id="snapshot-1",
    -        model_revision=3,
    -        model_id="embedding-1",
    -        connection_id="connection-1",
    -        driver_id="openai-compatible",
    -        driver_contract_version="1",
    -        auth_identity="account-1",
    -        connection_fingerprint="connection-digest",
    -        model="embed-a",
    -        dimensions=3,
    -        normalization="none",
    -        capability_digest="digest-embedding",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_discovers_chats_and_runs_nonstream_stream_and_embedding() -> None:
    -    with _provider() as (server, endpoint):
    -        credential = _Credential()
    -        driver = definition()
    -        assert driver.probe is not None and driver.discover is not None
    -        await driver.probe(_connection(endpoint), credential)
    -        discovered = await driver.discover(_connection(endpoint), credential)
    -        assert [(item.model, item.kind) for item in discovered] == [
    -            ("chat-a", ModelKind.CHAT)
    -        ]
    -
    -        opened = await driver.open(_connection(endpoint), credential)
    -        chat = opened.bind_chat(_chat_descriptor(), {"max_tool_schemas": 32})
    -        nonstream = await chat.complete(
    -            ModelRequest(
    -                messages=({"role": "user", "content": "hello"},),
    -                tools=({"type": "function", "function": {"name": "lookup"}},),
    -                system_prompt="system",
    -            )
    -        )
    -        assert nonstream.content is None
    -        assert nonstream.thinking == "checked"
    -        assert [(call.name, call.arguments) for call in nonstream.tool_calls] == [
    -            ("lookup", {"id": 7})
    -        ]
    -        assert nonstream.usage is not None
    -        assert nonstream.usage.coverage is UsageCoverage.EXACT
    -        assert nonstream.usage.reasoning_output_tokens == 1
    -        assert chat.max_tool_schemas == 32
    -
    -        alias_chat = opened.bind_chat(_chat_descriptor("reasoning-alias"), {})
    -        alias_response = await alias_chat.complete(
    -            ModelRequest(messages=({"role": "user", "content": "hello"},))
    -        )
    -        assert alias_response.thinking == "checked"
    -
    -        tagged_chat = opened.bind_chat(_chat_descriptor("think-tags"), {})
    -        tagged = await tagged_chat.complete(ModelRequest(messages=()))
    -        assert tagged.content == "answer"
    -        assert tagged.thinking == "checked tags"
    -
    -        tagged_deltas: list[dict[str, str]] = []
    -
    -        async def on_tagged_delta(delta: dict[str, str]) -> None:
    -            tagged_deltas.append(delta)
    -
    -        streamed_tagged = await tagged_chat.complete(
    -            ModelRequest(messages=(), on_delta=on_tagged_delta)
    -        )
    -        assert streamed_tagged.content == "answer"
    -        assert streamed_tagged.thinking == "checked tags"
    -        assert tagged_deltas == [
    -            {"thinking_delta": "checked tags"},
    -            {"content_delta": "answer"},
    -        ]
    -
    -        late_deltas: list[dict[str, str]] = []
    -
    -        async def on_late_delta(delta: dict[str, str]) -> None:
    -            late_deltas.append(delta)
    -
    -        late_chat = opened.bind_chat(_chat_descriptor("think-tags-native-late"), {})
    -        late = await late_chat.complete(
    -            ModelRequest(messages=(), on_delta=on_late_delta)
    -        )
    -        assert late.content == "legacyanswer"
    -        assert late.thinking == "native"
    -        assert late_deltas == [
    -            {"content_delta": "legacyanswer"},
    -            {"thinking_delta": "native"},
    -        ]
    -
    -        plain_deltas: list[dict[str, str]] = []
    -        plain_delta_received = asyncio.Event()
    -
    -        async def on_plain_delta(delta: dict[str, str]) -> None:
    -            plain_deltas.append(delta)
    -            plain_delta_received.set()
    -
    -        plain_chat = opened.bind_chat(_chat_descriptor("plain-stream"), {})
    -        plain_task = asyncio.create_task(
    -            plain_chat.complete(ModelRequest(messages=(), on_delta=on_plain_delta))
    -        )
    -        await asyncio.wait_for(plain_delta_received.wait(), 1)
    -        assert plain_deltas == [{"content_delta": "hello "}]
    -        server.release_plain_done.set()
    -        plain = await plain_task
    -        assert plain.content == "hello world"
    -        assert plain_deltas == [
    -            {"content_delta": "hello "},
    -            {"content_delta": "world"},
    -        ]
    -
    -        deltas: list[dict[str, str]] = []
    -
    -        async def on_delta(delta: dict[str, str]) -> None:
    -            deltas.append(delta)
    -
    -        stream_chat = opened.bind_chat(_chat_descriptor("stream-model"), {})
    -        streamed = await stream_chat.complete(
    -            ModelRequest(
    -                messages=({"role": "user", "content": "stream"},),
    -                on_delta=on_delta,
    -            )
    -        )
    -        assert streamed.content == "hello"
    -        assert streamed.thinking == "why because"
    -        assert deltas == [
    -            {"thinking_delta": "why "},
    -            {"thinking_delta": "because "},
    -            {"content_delta": "hello "},
    -        ]
    -        assert [
    -            (call.id, call.name, call.arguments) for call in streamed.tool_calls
    -        ] == [("call-1", "search", {"q": "hi"})]
    -        assert streamed.usage is not None
    -        assert streamed.usage.input_tokens == 10
    -        assert streamed.usage.cached_input_tokens == 3
    -
    -        embedding = opened.bind_embedding(_embedding_descriptor(), {})
    -        embedded = await embedding.embed(("first", "second"))
    -        assert embedded.vectors == (
    -            (1.0, 0.0, 0.0),
    -            (0.0, 1.0, 0.0),
    -        )
    -        assert embedded.usage is not None
    -        assert embedded.usage.coverage is UsageCoverage.PARTIAL
    -
    -        credential.token = "rotated"
    -        _ = await chat.complete(
    -            ModelRequest(messages=({"role": "user", "content": "again"},))
    -        )
    -        assert server.requests[-1]["authorization"] == "Bearer rotated"
    -        sent = next(
    -            request["body"]
    -            for request in server.requests
    -            if request.get("body", {}).get("model") == "chat-a"
    -        )
    -        assert sent["reasoning_effort"] == "high"
    -        assert sent["messages"][0] == {"role": "system", "content": "system"}
    -
    -
    -@pytest.mark.asyncio
    -async def test_embedding_uses_default_batch_limit_and_preserves_order() -> None:
    -    with _provider() as (server, endpoint):
    -        opened = await definition().open(_connection(endpoint), _Credential())
    -        with pytest.raises(ValueError, match="embedding_batch_size"):
    -            opened.bind_embedding(
    -                _embedding_descriptor(),
    -                {"embedding_batch_size": 0},
    -            )
    -        embedding = opened.bind_embedding(_embedding_descriptor(), {})
    -
    -        result = await embedding.embed(tuple(f"item-{index}" for index in range(23)))
    -
    -        requests = [
    -            request["body"]["input"]
    -            for request in server.requests
    -            if request["path"] == "/v1/embeddings"
    -        ]
    -        assert [len(batch) for batch in requests] == [10, 10, 3]
    -        assert result.vectors == tuple((float(index), 0.0, 0.0) for index in range(23))
    -        assert result.usage is not None
    -        assert result.usage.input_tokens == 12
    -        assert result.usage.request_count == 3
    -        assert result.usage.coverage is UsageCoverage.PARTIAL
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_maps_errors_and_preserves_cancellation() -> None:
    -    with _provider() as (server, endpoint):
    -        opened = await definition().open(_connection(endpoint), _Credential())
    -        for model, error_type in (
    -            ("context-error", ContextLengthError),
    -            ("rate-error", RateLimitError),
    -            ("quota-error", QuotaError),
    -            ("auth-error", AuthenticationError),
    -            ("invalid-error", InvalidRequestError),
    -        ):
    -            chat = opened.bind_chat(_chat_descriptor(model), {})
    -            with pytest.raises(error_type):
    -                await chat.complete(ModelRequest(messages=()))
    -
    -        slow = opened.bind_chat(_chat_descriptor("slow-model"), {})
    -        task = asyncio.create_task(slow.complete(ModelRequest(messages=())))
    -        assert await asyncio.to_thread(server.slow_started.wait, 1)
    -        task.cancel()
    -        with pytest.raises(asyncio.CancelledError):
    -            await task
    -
    -        class CallbackFailure(Exception):
    -            pass
    -
    -        async def fail_callback(_delta: dict[str, str]) -> None:
    -            raise CallbackFailure("consumer failed")
    -
    -        stream = opened.bind_chat(_chat_descriptor("stream-model"), {})
    -        with pytest.raises(CallbackFailure, match="consumer failed"):
    -            await stream.complete(ModelRequest(messages=(), on_delta=fail_callback))
    -
    -        emitted: list[dict[str, str]] = []
    -
    -        async def collect(delta: dict[str, str]) -> None:
    -            emitted.append(delta)
    -
    -        truncated = opened.bind_chat(_chat_descriptor("truncated-stream"), {})
    -        with pytest.raises(TransportError, match="terminal marker") as truncated_error:
    -            await truncated.complete(ModelRequest(messages=(), on_delta=collect))
    -        assert emitted
    -        assert truncated_error.value.retryable is False
    -
    -        leaked = opened.bind_chat(_chat_descriptor("echo-secret-error"), {})
    -        with pytest.raises(AuthenticationError) as caught:
    -            await leaked.complete(ModelRequest(messages=()))
    -        assert "secret" not in str(caught.value)
    -        assert "[REDACTED]" in str(caught.value)
    -
    -
    -@pytest.mark.asyncio
    -async def test_manual_gateway_does_not_require_models_catalog() -> None:
    -    with _provider(models_status=404) as (_server, endpoint):
    -        base = _connection(endpoint)
    -        descriptor = DriverConnectionDescriptor(
    -            connection_id=base.connection_id,
    -            name=base.name,
    -            driver_id=base.driver_id,
    -            endpoint=base.endpoint,
    -            auth_identity=base.auth_identity,
    -            config={
    -                "format_version": 1,
    -                "max_retries": 0,
    -                "allow_unverified_manual": True,
    -            },
    -        )
    -        credential = _Credential()
    -        await definition().probe(descriptor, credential)  # type: ignore[misc]
    -        opened = await definition().open(descriptor, credential)
    -        chat = opened.bind_chat(_chat_descriptor("manual-model"), {})
    -        response = await chat.complete(ModelRequest(messages=()))
    -        assert response.tool_calls[0].name == "lookup"
    -        with pytest.raises(InvalidRequestError, match="catalog unavailable"):
    -            await definition().discover(descriptor, credential)  # type: ignore[misc]
    -
    -    with _provider() as (_server, closed_endpoint):
    -        closed = _connection(closed_endpoint)
    -    with pytest.raises(TransportError):
    -        await definition().probe(closed, _Credential())  # type: ignore[misc]
    -
    -
    -@pytest.mark.asyncio
    -async def test_discovery_has_fixed_retry_size_and_entry_limits() -> None:
    -    driver = definition()
    -    assert driver.discover is not None
    -    credential = _Credential()
    -
    -    with _provider(model_ids=["compressed-model"], gzip_models=True) as (
    -        _server,
    -        endpoint,
    -    ):
    -        models = await driver.discover(_connection(endpoint), credential)
    -        assert [model.model for model in models] == ["compressed-model"]
    -
    -    with _provider(models_status=429) as (server, endpoint):
    -        base = _connection(endpoint)
    -        unbounded_request = DriverConnectionDescriptor(
    -            connection_id=base.connection_id,
    -            name=base.name,
    -            driver_id=base.driver_id,
    -            endpoint=base.endpoint,
    -            auth_identity=base.auth_identity,
    -            config={
    -                "format_version": 1,
    -                "max_retries": 1_000_000,
    -                "connect_timeout": 1_000_000,
    -                "read_timeout": 1_000_000,
    -            },
    -        )
    -        with pytest.raises(RateLimitError):
    -            await driver.discover(unbounded_request, credential)
    -        assert [item["path"] for item in server.requests] == ["/v1/models"]
    -
    -    with _provider(model_ids=[f"model-{index}" for index in range(10_001)]) as (
    -        _server,
    -        endpoint,
    -    ):
    -        with pytest.raises(TransportError, match="10000 entries"):
    -            await driver.discover(_connection(endpoint), credential)
    -
    -    with _provider(model_ids=["x" * (4 * 1024 * 1024)]) as (_server, endpoint):
    -        with pytest.raises(TransportError, match="exceeds 4194304 bytes"):
    -            await driver.discover(_connection(endpoint), credential)
    -
    -    with _provider(
    -        model_ids=["x" * (4 * 1024 * 1024)],
    -        gzip_models=True,
    -    ) as (_server, endpoint):
    -        with pytest.raises(TransportError, match="4194304 decoded bytes"):
    -            await driver.discover(_connection(endpoint), credential)
    -
    -    for model_ids, message in (
    -        (["duplicate", "duplicate"], "duplicate id"),
    -        ([" outer-space"], "outer whitespace"),
    -        (["x" * 257], "longer than 256"),
    -    ):
    -        with _provider(model_ids=model_ids) as (_server, endpoint):
    -            with pytest.raises(TransportError, match=message):
    -                await driver.discover(_connection(endpoint), credential)
    -
    -
    -@pytest.mark.asyncio
    -async def test_persisted_config_rejects_unbounded_secret_surfaces() -> None:
    -    with _provider() as (_server, endpoint):
    -        for config in (
    -            {"headers": {"X-API-Key": "secret"}},
    -            {"extra_body": {"password": "secret"}},
    -        ):
    -            with pytest.raises(ValueError):
    -                await definition().open(
    -                    DriverConnectionDescriptor(
    -                        connection_id="connection-1",
    -                        name="OpenAI",
    -                        driver_id="openai-compatible",
    -                        endpoint=endpoint,
    -                        auth_identity="account-1",
    -                        config=config,
    -                    ),
    -                    _Credential(),
    -                )
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_is_an_installable_ordinary_artifact(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    source = Path("plugins/openai_compatible")
    -    for path in source.glob("*.py"):
    -        tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    -        for node in ast.walk(tree):
    -            if isinstance(node, ast.Import):
    -                assert all(not item.name.startswith("plugins.") for item in node.names)
    -            elif isinstance(node, ast.ImportFrom) and node.module:
    -                assert not node.module.startswith("plugins.")
    -                if node.module.startswith("agent."):
    -                    assert node.module == "agent.plugin_composition"
    -
    -    repo = tmp_path / "driver-repo"
    -    shutil.copytree(source, repo)
    -    shutil.rmtree(repo / "__pycache__", ignore_errors=True)
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "initial"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -    installed = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    assert installed.plugin_name == "openai-compatible"
    -    assert installed.installed_path.is_relative_to(tmp_path / "home")
    -    assert installed.installed_path != source.resolve()
    -
    -    models_repo = tmp_path / "models-repo"
    -    shutil.copytree(Path("plugins/models"), models_repo)
    -    shutil.rmtree(models_repo / "__pycache__", ignore_errors=True)
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "initial"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=models_repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -    models_installed = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(models_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -
    -    blocked_prefixes = ("plugins.models", "plugins.openai_compatible")
    -    for module_name in tuple(sys.modules):
    -        if module_name.startswith(blocked_prefixes):
    -            monkeypatch.delitem(sys.modules, module_name)
    -
    -    class BlockRepositoryPlugins:
    -        def find_spec(
    -            self,
    -            fullname: str,
    -            path: object = None,
    -            target: object = None,
    -        ) -> None:
    -            _ = path, target
    -            if fullname.startswith(blocked_prefixes):
    -                raise ModuleNotFoundError(
    -                    f"repository plugin import blocked: {fullname}"
    -                )
    -            return None
    -
    -    monkeypatch.setattr(sys, "meta_path", [BlockRepositoryPlugins(), *sys.meta_path])
    -    with _provider() as (_server, endpoint):
    -        manager = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await manager.load_all()
    -        for plugin_id, expected_path in (
    -            ("models@ordinary-test", models_installed.installed_path),
    -            ("openai-compatible@ordinary-test", installed.installed_path),
    -        ):
    -            generation = manager.generation(plugin_id)
    -            assert generation is not None and generation.source_type == "installed"
    -            assert (
    -                Path(generation.instance.module.__file__)
    -                .resolve()
    -                .is_relative_to(expected_path)
    -            )
    -
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        root = snapshot.composition_root
    -        lease = await manager._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            settings = root.context.require(MODEL_SETTINGS)
    -            revision = (
    -                await settings.apply(
    -                    AddConnection(
    -                        expected_revision=0,
    -                        connection_id="openai",
    -                        name="OpenAI",
    -                        driver_id="openai-compatible",
    -                        endpoint=endpoint,
    -                        auth_identity="account",
    -                        credential={"driver": "api_key", "access_token": "secret"},
    -                        driver_config={"max_retries": 0},
    -                    )
    -                )
    -            ).revision
    -            revision = (
    -                await settings.apply(
    -                    SyncModels(expected_revision=revision, connection_id="openai")
    -                )
    -            ).revision
    -            catalog = root.context.require(MODEL_CATALOG).snapshot()
    -            chat_id = next(
    -                model.model_id for model in catalog.models if model.model == "chat-a"
    -            )
    -            revision = (
    -                await settings.apply(
    -                    SetDefaultModel(
    -                        expected_revision=revision,
    -                        role=ModelRole.DEFAULT,
    -                        model_id=chat_id,
    -                    )
    -                )
    -            ).revision
    -            revision = (
    -                await settings.apply(
    -                    AddModel(
    -                        expected_revision=revision,
    -                        model_id="embedding",
    -                        connection_id="openai",
    -                        kind=ModelKind.EMBEDDING,
    -                        model="embedding-a",
    -                        capabilities=ModelCapabilities(embedding_dimensions=3),
    -                        capability_sources=CapabilitySources(
    -                            embedding_dimensions="manual"
    -                        ),
    -                    )
    -                )
    -            ).revision
    -            _ = await settings.apply(
    -                SetDefaultModel(
    -                    expected_revision=revision,
    -                    role=None,
    -                    model_id="embedding",
    -                )
    -            )
    -
    -            async with root.context.require(CHAT_MODELS).execution() as execution:
    -                chat = execution.chat(ModelRole.DEFAULT)
    -                response = await chat.complete(
    -                    ModelRequest(messages=({"role": "user", "content": "hello"},))
    -                )
    -                assert response.thinking == "checked"
    -                assert response.tool_calls[0].name == "lookup"
    -                deltas: list[dict[str, str]] = []
    -
    -                async def on_delta(delta: dict[str, str]) -> None:
    -                    deltas.append(delta)
    -
    -                streamed = await chat.complete(
    -                    ModelRequest(
    -                        messages=({"role": "user", "content": "stream"},),
    -                        on_delta=on_delta,
    -                    )
    -                )
    -                assert streamed.thinking == "why because"
    -                assert streamed.tool_calls[0].name == "search"
    -                assert streamed.usage is not None
    -                assert streamed.usage.coverage is UsageCoverage.EXACT
    -                assert deltas == [
    -                    {"thinking_delta": "why "},
    -                    {"thinking_delta": "because "},
    -                    {"content_delta": "hello "},
    -                ]
    -            async with root.context.require(EMBEDDINGS).bind() as embedding:
    -                result = await embedding.embed(("first", "second"))
    -                assert len(result.vectors) == 2
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -            await manager.terminate_all()
    -
    -        reloaded = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await reloaded.load_all()
    -        snapshot = reloaded.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.AVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG)
    -            .snapshot()
    -            .models
    -        )
    -        await reloaded.terminate_all()
    -
    -        set_installed_plugin_enabled(
    -            "openai-compatible@ordinary-test",
    -            enabled=False,
    -            plugins_home=tmp_path / "home",
    -        )
    -        _ = finalize_uninstall_plugin(
    -            "openai-compatible@ordinary-test",
    -            workspace=tmp_path / "workspace",
    -            plugins_home=tmp_path / "home",
    -        )
    -        without_driver = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await without_driver.load_all()
    -        snapshot = without_driver.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.DRIVER_UNAVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG)
    -            .snapshot()
    -            .models
    -        )
    -        await without_driver.terminate_all()
    -
    -        restored_install = install_git_plugin(
    -            workspace=tmp_path / "workspace",
    -            source=str(repo),
    -            marketplace="ordinary-test",
    -            plugins_home=tmp_path / "home",
    -        )
    -        restored = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await restored.load_all()
    -        generation = restored.generation("openai-compatible@ordinary-test")
    -        assert generation is not None
    -        assert generation.plugin_dir == restored_install.installed_path
    -        snapshot = restored.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.AVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG)
    -            .snapshot()
    -            .models
    -        )
    -        await restored.terminate_all()
    -
    -    assert not any(name.startswith(blocked_prefixes) for name in sys.modules)
    diff --git a/tests/test_opencode_go_model_plugin.py b/tests/test_opencode_go_model_plugin.py
    deleted file mode 100644
    index 46f21f4ed..000000000
    --- a/tests/test_opencode_go_model_plugin.py
    +++ /dev/null
    @@ -1,1050 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import ast
    -import json
    -import os
    -import shutil
    -import sqlite3
    -import subprocess
    -import sys
    -import threading
    -import time
    -from collections.abc import AsyncIterator, Mapping
    -from contextlib import asynccontextmanager, contextmanager
    -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    -from dataclasses import replace
    -from pathlib import Path
    -from typing import Any, Iterator
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    AddConnection,
    -    AuthenticationError,
    -    BoundModelDescriptor,
    -    CapabilitySources,
    -    CHAT_MODELS,
    -    ContextLengthError,
    -    DriverConnectionDescriptor,
    -    ModelCapabilities,
    -    ModelAvailability,
    -    MODEL_CATALOG,
    -    MODEL_SETTINGS,
    -    InvalidRequestError,
    -    ModelKind,
    -    ModelRequest,
    -    ModelRole,
    -    RateLimitError,
    -    QuotaError,
    -    SetDefaultModel,
    -    SyncModels,
    -    TransportError,
    -    UsageCoverage,
    -)
    -from agent.plugins.install import (
    -    finalize_uninstall_plugin,
    -    install_git_plugin,
    -    set_installed_plugin_enabled,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from bus.event_bus import EventBus
    -from plugins.opencode_go import driver as opencode_driver
    -from plugins.opencode_go.driver import _parse_cli_catalog, definition
    -
    -
    -class _Credential:
    -    def __init__(self, token: str = "secret") -> None:
    -        self.connection_id = "connection-1"
    -        self.auth_identity = "account-1"
    -        self.token = token
    -
    -    async def read(self) -> Mapping[str, str]:
    -        return {"driver": "api_key", "access_token": self.token}
    -
    -    async def refresh(self, payload: Mapping[str, str]) -> None:
    -        self.token = payload["access_token"]
    -
    -    @asynccontextmanager
    -    async def exclusive(self) -> AsyncIterator[None]:
    -        yield
    -
    -
    -@pytest.fixture(autouse=True)
    -def _disable_host_opencode_cli(monkeypatch: pytest.MonkeyPatch) -> None:
    -    monkeypatch.setattr(
    -        opencode_driver,
    -        "_OPENCODE_EXECUTABLE",
    -        "/missing/opencode",
    -    )
    -
    -
    -def test_cli_catalog_parser_keeps_provider_owned_limits_and_variants() -> None:
    -    parsed = _parse_cli_catalog(
    -        """opencode-go/deepseek-v4-pro
    -{"limit":{"context":1000000,"output":384000},"variants":{"high":{},"max":{}}}
    -opencode-go/glm-5
    -{"limit":{"context":202752,"output":32768},"variants":{}}
    -"""
    -    )
    -    assert parsed["deepseek-v4-pro"]["limit"] == {
    -        "context": 1_000_000,
    -        "output": 384_000,
    -    }
    -    assert tuple(parsed["deepseek-v4-pro"]["variants"]) == ("high", "max")
    -
    -
    -@pytest.mark.asyncio
    -async def test_optional_cli_failures_degrade_and_reap_process(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    class FakeProcess:
    -        def __init__(self, mode: str) -> None:
    -            self.mode = mode
    -            self.returncode = 1 if mode == "nonzero" else 0
    -            self.killed = False
    -            self.waited = False
    -
    -        async def communicate(self) -> tuple[bytes, bytes]:
    -            if self.mode == "timeout":
    -                raise TimeoutError
    -            if self.mode == "cancel":
    -                raise asyncio.CancelledError
    -            if self.mode == "bad-utf8":
    -                return b"\xff", b""
    -            if self.mode == "bad-output":
    -                return b"not-a-catalog", b""
    -            return b"", b"command failed"
    -
    -        def kill(self) -> None:
    -            self.killed = True
    -
    -        async def wait(self) -> int:
    -            self.waited = True
    -            return 0
    -
    -    current = FakeProcess("nonzero")
    -
    -    async def create_process(*_args: object, **_kwargs: object) -> FakeProcess:
    -        return current
    -
    -    monkeypatch.setattr(asyncio, "create_subprocess_exec", create_process)
    -    for mode in ("nonzero", "timeout", "bad-utf8", "bad-output"):
    -        current = FakeProcess(mode)
    -        assert await opencode_driver._load_cli_catalog() == {}
    -        if mode == "timeout":
    -            assert current.killed and current.waited
    -
    -    current = FakeProcess("cancel")
    -    with pytest.raises(asyncio.CancelledError):
    -        await opencode_driver._load_cli_catalog()
    -    assert current.killed and current.waited
    -
    -
    -class _Server(ThreadingHTTPServer):
    -    daemon_threads = True
    -
    -    def __init__(self, address: tuple[str, int], *, models_status: int = 200) -> None:
    -        super().__init__(address, _Handler)
    -        self.requests: list[dict[str, Any]] = []
    -        self.slow_started = threading.Event()
    -        self.models_status = models_status
    -
    -
    -class _Handler(BaseHTTPRequestHandler):
    -    server: _Server
    -
    -    def log_message(self, format: str, *args: object) -> None:
    -        _ = format, args
    -
    -    def do_GET(self) -> None:
    -        self.server.requests.append(
    -            {"path": self.path, "authorization": self.headers.get("Authorization")}
    -        )
    -        if self.path == "/v1/models":
    -            self._json(
    -                self.server.models_status,
    -                (
    -                    {
    -                        "object": "list",
    -                        "data": [
    -                            {"id": "chat-a", "variants": {"high": {}, "max": {}}},
    -                            {"id": "qwen3.5-plus"},
    -                            {"id": "minimax-m2"},
    -                        ],
    -                    }
    -                    if self.server.models_status == 200
    -                    else {"error": {"message": "catalog unavailable"}}
    -                ),
    -            )
    -            return
    -        self._json(404, {"error": {"message": "missing"}})
    -
    -    def do_POST(self) -> None:
    -        length = int(self.headers.get("Content-Length") or 0)
    -        body = json.loads(self.rfile.read(length) or b"{}")
    -        self.server.requests.append(
    -            {
    -                "path": self.path,
    -                "authorization": self.headers.get("Authorization"),
    -                "body": body,
    -            }
    -        )
    -        if self.path != "/v1/chat/completions":
    -            self._json(404, {"error": {"message": "missing"}})
    -            return
    -        model = body.get("model")
    -        if model == "context-error":
    -            self._json(400, {"error": {"message": "maximum context length exceeded"}})
    -            return
    -        if model == "rate-error":
    -            self._json(429, {"error": {"message": "rate limited"}})
    -            return
    -        if model == "quota-error":
    -            self._json(429, {"error": {"message": "insufficient quota"}})
    -            return
    -        if model == "auth-error":
    -            self._json(401, {"error": {"message": "invalid token"}})
    -            return
    -        if model == "echo-secret-error":
    -            self._json(
    -                401,
    -                {"error": {"message": f"rejected {self.headers.get('Authorization')}"}},
    -            )
    -            return
    -        if model == "invalid-error":
    -            self._json(400, {"error": {"message": "unknown model"}})
    -            return
    -        if model == "slow-model":
    -            self.server.slow_started.set()
    -            time.sleep(2)
    -            self._json(200, _text_response("late"))
    -            return
    -        if body.get("stream"):
    -            self.send_response(200)
    -            self.send_header("Content-Type", "text/event-stream")
    -            self.end_headers()
    -            chunks = (
    -                {"choices": [{"delta": {"reasoning_content": "why "}}]},
    -                {"choices": [{"delta": {"reasoning": "because "}}]},
    -                {"choices": [{"delta": {"content": "hello "}}]},
    -                {
    -                    "choices": [
    -                        {
    -                            "delta": {
    -                                "tool_calls": [
    -                                    {
    -                                        "index": 0,
    -                                        "id": "call-1",
    -                                        "function": {"name": "search", "arguments": '{"q":'},
    -                                    }
    -                                ]
    -                            }
    -                        }
    -                    ]
    -                },
    -                {
    -                    "choices": [
    -                        {
    -                            "delta": {
    -                                "tool_calls": [
    -                                    {"index": 0, "function": {"arguments": '"hi"}'}}
    -                                ]
    -                            },
    -                            "finish_reason": "tool_calls",
    -                        }
    -                    ]
    -                },
    -                {
    -                    "choices": [],
    -                    "usage": {
    -                        "prompt_cache_hit_tokens": 3,
    -                        "prompt_cache_miss_tokens": 7,
    -                        "completion_tokens": 4,
    -                    },
    -                },
    -            )
    -            if model == "invalid-tool-stream":
    -                chunks = (
    -                    {"choices": [{"delta": {"content": "visible "}}]},
    -                    {
    -                        "choices": [
    -                            {
    -                                "delta": {
    -                                    "tool_calls": [
    -                                        {
    -                                            "index": 0,
    -                                            "id": "call-bad",
    -                                            "function": {
    -                                                "name": "search",
    -                                                "arguments": '{"q":',
    -                                            },
    -                                        }
    -                                    ]
    -                                }
    -                            }
    -                        ]
    -                    },
    -                )
    -            for chunk in chunks:
    -                self.wfile.write(f"data: {json.dumps(chunk)}\n\n".encode())
    -            if model != "truncated-stream":
    -                self.wfile.write(b"data: [DONE]\n\n")
    -            self.wfile.flush()
    -            return
    -        reasoning_field = (
    -            "reasoning" if model == "reasoning-alias" else "reasoning_content"
    -        )
    -        self._json(
    -            200,
    -            {
    -                "choices": [
    -                    {
    -                        "message": {
    -                            "content": None,
    -                            reasoning_field: "checked",
    -                            "tool_calls": [
    -                                {
    -                                    "id": "call-2",
    -                                    "function": {
    -                                        "name": "lookup",
    -                                        "arguments": '{"id": 7}',
    -                                    },
    -                                }
    -                            ],
    -                        },
    -                        "finish_reason": "tool_calls",
    -                    }
    -                ],
    -                "usage": {
    -                    "prompt_tokens": 8,
    -                    "completion_tokens": 2,
    -                    "completion_tokens_details": {"reasoning_tokens": 1},
    -                },
    -            },
    -        )
    -
    -    def _json(self, status: int, payload: Mapping[str, Any]) -> None:
    -        encoded = json.dumps(payload).encode()
    -        self.send_response(status)
    -        self.send_header("Content-Type", "application/json")
    -        self.send_header("Content-Length", str(len(encoded)))
    -        self.end_headers()
    -        try:
    -            self.wfile.write(encoded)
    -        except BrokenPipeError:
    -            pass
    -
    -
    -def _text_response(content: str) -> dict[str, Any]:
    -    return {
    -        "choices": [{"message": {"content": content}, "finish_reason": "stop"}],
    -        "usage": {"prompt_tokens": 1, "completion_tokens": 1},
    -    }
    -
    -
    -@contextmanager
    -def _provider(*, models_status: int = 200) -> Iterator[tuple[_Server, str]]:
    -    server = _Server(("127.0.0.1", 0), models_status=models_status)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    try:
    -        yield server, f"http://127.0.0.1:{server.server_port}/v1"
    -    finally:
    -        server.shutdown()
    -        server.server_close()
    -        thread.join(timeout=2)
    -
    -
    -def _connection(endpoint: str) -> DriverConnectionDescriptor:
    -    return DriverConnectionDescriptor(
    -        connection_id="connection-1",
    -        name="Test",
    -        driver_id="opencode-go",
    -        endpoint=endpoint,
    -        auth_identity="account-1",
    -        config={
    -            "format_version": 1,
    -            "max_retries": 0,
    -        },
    -    )
    -
    -
    -def _chat_descriptor(model: str = "chat-a") -> BoundModelDescriptor:
    -    return BoundModelDescriptor(
    -        binding_id=f"binding-{model}",
    -        plugin_snapshot_id="snapshot-1",
    -        model_revision=3,
    -        model_id=f"model-{model}",
    -        connection_id="connection-1",
    -        driver_id="opencode-go",
    -        driver_contract_version="1",
    -        auth_identity="account-1",
    -        model=model,
    -        role=ModelRole.DEFAULT,
    -        reasoning_effort="high",
    -        capabilities=ModelCapabilities(context_window=8192, supports_tool_calls=True),
    -        capability_sources=CapabilitySources(context_window="test"),
    -        capability_digest="digest-chat",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_discovers_and_runs_opencode_go_chat_contract() -> None:
    -    with _provider() as (server, endpoint):
    -        credential = _Credential()
    -        driver = definition()
    -        assert driver.probe is not None and driver.discover is not None
    -        await driver.probe(_connection(endpoint), credential)
    -        discovered = await driver.discover(_connection(endpoint), credential)
    -        assert [(item.model, item.kind) for item in discovered] == [
    -            ("chat-a", ModelKind.CHAT)
    -        ]
    -        assert discovered[0].capabilities.supported_reasoning_efforts == ()
    -        assert discovered[0].capabilities.supports_tool_calls is None
    -        assert discovered[0].capability_sources.tool_calls == "unknown"
    -        assert discovered[0].capability_sources.input_modalities == "unknown"
    -
    -        opened = await driver.open(_connection(endpoint), credential)
    -        chat = opened.bind_chat(_chat_descriptor(), {})
    -        nonstream = await chat.complete(
    -            ModelRequest(
    -                messages=({"role": "user", "content": "hello"},),
    -                tools=({"type": "function", "function": {"name": "lookup"}},),
    -                system_prompt="system",
    -            )
    -        )
    -        assert nonstream.content is None
    -        assert nonstream.thinking == "checked"
    -        assert [(call.name, call.arguments) for call in nonstream.tool_calls] == [
    -            ("lookup", {"id": 7})
    -        ]
    -        assert nonstream.usage is not None
    -        assert nonstream.usage.coverage is UsageCoverage.EXACT
    -        assert nonstream.usage.reasoning_output_tokens == 1
    -        assert chat.max_tool_schemas == 16
    -
    -        alias_chat = opened.bind_chat(_chat_descriptor("reasoning-alias"), {})
    -        alias_response = await alias_chat.complete(
    -            ModelRequest(messages=({"role": "user", "content": "hello"},))
    -        )
    -        assert alias_response.thinking == "checked"
    -
    -        deltas: list[dict[str, str]] = []
    -
    -        async def on_delta(delta: dict[str, str]) -> None:
    -            deltas.append(delta)
    -
    -        stream_chat = opened.bind_chat(_chat_descriptor("stream-model"), {})
    -        streamed = await stream_chat.complete(
    -            ModelRequest(
    -                messages=({"role": "user", "content": "stream"},),
    -                on_delta=on_delta,
    -            )
    -        )
    -        assert streamed.content == "hello"
    -        assert streamed.thinking == "why because"
    -        assert deltas == [
    -            {"thinking_delta": "why "},
    -            {"thinking_delta": "because "},
    -            {"content_delta": "hello "},
    -        ]
    -        assert [(call.id, call.name, call.arguments) for call in streamed.tool_calls] == [
    -            ("call-1", "search", {"q": "hi"})
    -        ]
    -        assert streamed.usage is not None
    -        assert streamed.usage.input_tokens == 10
    -        assert streamed.usage.cached_input_tokens == 3
    -
    -        credential.token = "rotated"
    -        _ = await chat.complete(ModelRequest(messages=({"role": "user", "content": "again"},)))
    -        assert server.requests[-1]["authorization"] == "Bearer rotated"
    -        sent = next(
    -            request["body"]
    -            for request in server.requests
    -            if request.get("body", {}).get("model") == "chat-a"
    -        )
    -        assert sent["reasoning_effort"] == "high"
    -        assert sent["messages"][0] == {"role": "system", "content": "system"}
    -
    -
    -@pytest.mark.asyncio
    -async def test_five_wire_profiles_and_messages_models_are_owned_by_driver() -> None:
    -    with _provider() as (server, endpoint):
    -        opened = await definition().open(_connection(endpoint), _Credential())
    -        profiles = (
    -            ("future-chat", "high", 10),
    -            ("deepseek-v4-pro", "max", 10),
    -            ("glm-5", "high", 10),
    -            ("kimi-k3", "high", 10),
    -            ("mimo-v2.5-pro", "high", 131_072),
    -        )
    -        for model, effort, max_tokens in profiles:
    -            descriptor = replace(_chat_descriptor(model), reasoning_effort=effort)
    -            chat = opened.bind_chat(descriptor, {})
    -            await chat.complete(
    -                ModelRequest(
    -                    messages=(
    -                        {"role": "assistant", "content": "old"},
    -                        {"role": "user", "content": "again"},
    -                    ),
    -                    tools=(
    -                        {
    -                            "type": "function",
    -                            "function": {
    -                                "name": "probe",
    -                                "strict": True,
    -                                "parameters": {
    -                                    "type": "object",
    -                                    "additionalProperties": False,
    -                                    "properties": {
    -                                        "value": {"type": ["string", "null"]}
    -                                    },
    -                                },
    -                            },
    -                        },
    -                    ),
    -                    max_output_tokens=200_000 if model.startswith("mimo-") else max_tokens,
    -                )
    -            )
    -        bodies = {item["body"]["model"]: item["body"] for item in server.requests if "body" in item}
    -        assert bodies["future-chat"]["reasoning_effort"] == "high"
    -        assert bodies["deepseek-v4-pro"]["thinking"] == {"type": "enabled"}
    -        assert bodies["deepseek-v4-pro"]["reasoning_effort"] == "max"
    -        assert bodies["deepseek-v4-pro"]["messages"][0]["reasoning_content"] == ""
    -        assert bodies["glm-5"]["reasoning_effort"] == "high"
    -        assert bodies["kimi-k3"]["reasoning_effort"] == "high"
    -        assert bodies["mimo-v2.5-pro"]["max_tokens"] == 131_072
    -        function = bodies["future-chat"]["tools"][0]["function"]
    -        assert "strict" not in function
    -        assert "additionalProperties" not in function["parameters"]
    -        assert function["parameters"]["properties"]["value"]["type"] == "string"
    -
    -        for messages_model in ("qwen3.5-plus", "minimax-m2"):
    -            with pytest.raises(InvalidRequestError, match="Messages API"):
    -                opened.bind_chat(_chat_descriptor(messages_model), {})
    -
    -        named = opened.bind_chat(
    -            replace(_chat_descriptor("deepseek-v4-pro"), reasoning_effort="max"),
    -            {},
    -        )
    -        await named.complete(
    -            ModelRequest(
    -                messages=(),
    -                tools=({"type": "function", "function": {"name": "probe"}},),
    -                tool_choice={"type": "function", "function": {"name": "probe"}},
    -            )
    -        )
    -        assert server.requests[-1]["body"]["thinking"] == {"type": "disabled"}
    -        assert "reasoning_effort" not in server.requests[-1]["body"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_auth_imports_api_key_sqlite_and_legacy_in_owner_order(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    driver = definition()
    -    assert driver.start_auth is not None and driver.finish_auth is not None
    -    direct = await driver.start_auth({"api_key": "direct", "endpoint": "https://example.test/v1"})
    -    completed = await driver.finish_auth(direct["state"])
    -    assert completed["credential"] == {"driver": "api_key", "access_token": "direct"}
    -    with pytest.raises(ValueError, match="unsupported auth fields"):
    -        await driver.start_auth({"database_path": "/tmp/secret"})
    -    with pytest.raises(ValueError, match="official endpoint"):
    -        await driver.start_auth({"endpoint": "https://attacker.test/v1"})
    -
    -    database = tmp_path / "opencode.db"
    -    monkeypatch.setattr(opencode_driver, "_OPENCODE_DATA_DIR", tmp_path)
    -    connection = sqlite3.connect(database)
    -    connection.execute(
    -        "CREATE TABLE credential ("
    -        "id TEXT PRIMARY KEY, integration_id TEXT, value TEXT NOT NULL, "
    -        "active INTEGER, time_created INTEGER NOT NULL, time_updated INTEGER NOT NULL)"
    -    )
    -    connection.execute(
    -        "INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?)",
    -        (
    -            "old",
    -            "opencode-go",
    -            json.dumps({"type": "key", "key": "old-key"}),
    -            1,
    -            1,
    -            1,
    -        ),
    -    )
    -    connection.execute(
    -        "INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?)",
    -        (
    -            "inactive-newest",
    -            "opencode-go",
    -            json.dumps({"type": "key", "key": "inactive-key"}),
    -            0,
    -            3,
    -            3,
    -        ),
    -    )
    -    connection.execute(
    -        "INSERT INTO credential VALUES (?, ?, ?, ?, ?, ?)",
    -        (
    -            "new",
    -            "opencode-go",
    -            json.dumps({"type": "key", "key": "database-key"}),
    -            1,
    -            2,
    -            2,
    -        ),
    -    )
    -    connection.commit()
    -    connection.close()
    -    legacy = tmp_path / "auth.json"
    -    legacy.write_text(json.dumps({"opencode-go": {"key": "legacy-key"}}), encoding="utf-8")
    -    imported = await driver.start_auth({})
    -    result = await driver.finish_auth(imported["state"])
    -    assert result["credential"]["access_token"] == "database-key"
    -
    -    connection = sqlite3.connect(database)
    -    connection.execute("UPDATE credential SET value = ? WHERE id = 'new'", ("not-json",))
    -    connection.commit()
    -    connection.close()
    -    with pytest.raises(AuthenticationError, match="database credential is invalid"):
    -        await driver.start_auth({})
    -
    -    database.unlink()
    -    imported = await driver.start_auth({})
    -    result = await driver.finish_auth(imported["state"])
    -    assert result["credential"]["access_token"] == "legacy-key"
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_maps_errors_and_preserves_cancellation() -> None:
    -    with _provider() as (server, endpoint):
    -        opened = await definition().open(_connection(endpoint), _Credential())
    -        for model, error_type in (
    -            ("context-error", ContextLengthError),
    -            ("rate-error", RateLimitError),
    -            ("quota-error", QuotaError),
    -            ("auth-error", AuthenticationError),
    -            ("invalid-error", InvalidRequestError),
    -        ):
    -            chat = opened.bind_chat(_chat_descriptor(model), {})
    -            with pytest.raises(error_type):
    -                await chat.complete(ModelRequest(messages=()))
    -
    -        slow = opened.bind_chat(_chat_descriptor("slow-model"), {})
    -        task = asyncio.create_task(slow.complete(ModelRequest(messages=())))
    -        assert await asyncio.to_thread(server.slow_started.wait, 1)
    -        task.cancel()
    -        with pytest.raises(asyncio.CancelledError):
    -            await task
    -
    -        class CallbackFailure(Exception):
    -            pass
    -
    -        async def fail_callback(_delta: dict[str, str]) -> None:
    -            raise CallbackFailure("consumer failed")
    -
    -        stream = opened.bind_chat(_chat_descriptor("stream-model"), {})
    -        with pytest.raises(CallbackFailure, match="consumer failed"):
    -            await stream.complete(ModelRequest(messages=(), on_delta=fail_callback))
    -
    -        emitted: list[dict[str, str]] = []
    -
    -        async def collect(delta: dict[str, str]) -> None:
    -            emitted.append(delta)
    -
    -        truncated = opened.bind_chat(_chat_descriptor("truncated-stream"), {})
    -        with pytest.raises(TransportError, match="terminal marker") as truncated_error:
    -            await truncated.complete(ModelRequest(messages=(), on_delta=collect))
    -        assert emitted
    -        assert truncated_error.value.retryable is False
    -
    -        retrying_connection = replace(
    -            _connection(endpoint),
    -            config={"format_version": 1, "max_retries": 2},
    -        )
    -        retrying_opened = await definition().open(retrying_connection, _Credential())
    -        invalid_tools = retrying_opened.bind_chat(
    -            _chat_descriptor("invalid-tool-stream"),
    -            {},
    -        )
    -        before = len(server.requests)
    -        with pytest.raises(TransportError) as invalid_tool_error:
    -            await invalid_tools.complete(ModelRequest(messages=(), on_delta=collect))
    -        matching = [
    -            item
    -            for item in server.requests[before:]
    -            if item.get("body", {}).get("model") == "invalid-tool-stream"
    -        ]
    -        assert len(matching) == 1
    -        assert invalid_tool_error.value.retryable is False
    -
    -        leaked = opened.bind_chat(_chat_descriptor("echo-secret-error"), {})
    -        with pytest.raises(AuthenticationError) as caught:
    -            await leaked.complete(ModelRequest(messages=()))
    -        assert "secret" not in str(caught.value)
    -        assert "[REDACTED]" in str(caught.value)
    -
    -
    -@pytest.mark.asyncio
    -async def test_catalog_is_required() -> None:
    -    with _provider(models_status=404) as (_server, endpoint):
    -        with pytest.raises(InvalidRequestError, match="catalog unavailable"):
    -            await definition().probe(_connection(endpoint), _Credential())  # type: ignore[misc]
    -
    -    with _provider() as (_server, closed_endpoint):
    -        closed = _connection(closed_endpoint)
    -    with pytest.raises(TransportError):
    -        await definition().probe(closed, _Credential())  # type: ignore[misc]
    -
    -
    -@pytest.mark.asyncio
    -async def test_persisted_config_rejects_unbounded_secret_surfaces() -> None:
    -    with _provider() as (_server, endpoint):
    -        for config in (
    -            {"headers": {"X-API-Key": "secret"}},
    -            {"extra_body": {"password": "secret"}},
    -            {"opencode_executable": "/tmp/evil"},
    -            {"catalog_provider_id": "openai"},
    -        ):
    -            with pytest.raises(ValueError):
    -                await definition().open(
    -                    DriverConnectionDescriptor(
    -                        connection_id="connection-1",
    -                        name="OpenCode Go",
    -                        driver_id="opencode-go",
    -                        endpoint=endpoint,
    -                        auth_identity="account-1",
    -                        config=config,
    -                    ),
    -                    _Credential(),
    -                )
    -
    -        opened = await definition().open(_connection(endpoint), _Credential())
    -        for compatible in (
    -            {},
    -            {"format_version": 1},
    -            {"use_responses_lite": False, "reasoning_summary": "none"},
    -            {"use_responses_lite": 0, "reasoning_summary": ""},
    -            {"max_tool_schemas": 16},
    -            {"max_tool_schemas": 32},
    -            {"max_tool_schemas": None},
    -        ):
    -            assert opened.bind_chat(_chat_descriptor(), compatible).max_tool_schemas == 16
    -        for incompatible in (
    -            {"max_tool_schemas": 0},
    -            {"max_tool_schemas": True},
    -            {"max_tool_schemas": "16"},
    -            {"use_responses_lite": True},
    -            {"reasoning_summary": "auto"},
    -        ):
    -            with pytest.raises(ValueError):
    -                opened.bind_chat(_chat_descriptor(), incompatible)
    -
    -
    -@pytest.mark.asyncio
    -async def test_driver_is_an_installable_ordinary_artifact(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    source = Path("plugins/opencode_go")
    -    for path in source.glob("*.py"):
    -        tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    -        for node in ast.walk(tree):
    -            if isinstance(node, ast.Import):
    -                assert all(not item.name.startswith("plugins.") for item in node.names)
    -            elif isinstance(node, ast.ImportFrom) and node.module:
    -                assert not node.module.startswith("plugins.")
    -                if node.module.startswith("agent."):
    -                    assert node.module == "agent.plugin_composition"
    -
    -    repo = tmp_path / "driver-repo"
    -    shutil.copytree(source, repo)
    -    shutil.rmtree(repo / "__pycache__", ignore_errors=True)
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "initial"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -    installed = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -    assert installed.plugin_name == "opencode-go"
    -    assert installed.installed_path.is_relative_to(tmp_path / "home")
    -    assert installed.installed_path != source.resolve()
    -
    -    models_repo = tmp_path / "models-repo"
    -    shutil.copytree(Path("plugins/models"), models_repo)
    -    shutil.rmtree(models_repo / "__pycache__", ignore_errors=True)
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "initial"),
    -    ):
    -        result = subprocess.run(
    -            ("git", *args),
    -            cwd=models_repo,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -        )
    -        assert result.returncode == 0, result.stderr
    -    models_installed = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(models_repo),
    -        marketplace="ordinary-test",
    -        plugins_home=tmp_path / "home",
    -    )
    -
    -    blocked_prefixes = ("plugins.models", "plugins.opencode_go")
    -    for module_name in tuple(sys.modules):
    -        if module_name.startswith(blocked_prefixes):
    -            monkeypatch.delitem(sys.modules, module_name)
    -
    -    class BlockRepositoryPlugins:
    -        def find_spec(
    -            self,
    -            fullname: str,
    -            path: object = None,
    -            target: object = None,
    -        ) -> None:
    -            _ = path, target
    -            if fullname.startswith(blocked_prefixes):
    -                raise ModuleNotFoundError(f"repository plugin import blocked: {fullname}")
    -            return None
    -
    -    monkeypatch.setattr(sys, "meta_path", [BlockRepositoryPlugins(), *sys.meta_path])
    -    original_path = os.environ.get("PATH", "")
    -    empty_path = tmp_path / "empty-path"
    -    empty_path.mkdir()
    -    with _provider() as (_server, endpoint):
    -        manager = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await manager.load_all()
    -        for plugin_id, expected_path in (
    -            ("models@ordinary-test", models_installed.installed_path),
    -            ("opencode-go@ordinary-test", installed.installed_path),
    -        ):
    -            generation = manager.generation(plugin_id)
    -            assert generation is not None and generation.source_type == "installed"
    -            assert Path(generation.instance.module.__file__).resolve().is_relative_to(
    -                expected_path
    -            )
    -
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        root = snapshot.composition_root
    -        lease = await manager._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            monkeypatch.setenv("PATH", str(empty_path))
    -            settings = root.context.require(MODEL_SETTINGS)
    -            revision = (
    -                await settings.apply(
    -                    AddConnection(
    -                        expected_revision=0,
    -                        connection_id="opencode-go",
    -                        name="OpenCode Go",
    -                        driver_id="opencode-go",
    -                        endpoint=endpoint,
    -                        auth_identity="account",
    -                        credential={"driver": "api_key", "access_token": "secret"},
    -                        driver_config={
    -                            "max_retries": 0,
    -                            "catalog_provider_id": "opencode-go",
    -                        },
    -                    )
    -                )
    -            ).revision
    -            revision = (
    -                await settings.apply(
    -                    SyncModels(expected_revision=revision, connection_id="opencode-go")
    -                )
    -            ).revision
    -            monkeypatch.setenv("PATH", original_path)
    -            catalog = root.context.require(MODEL_CATALOG).snapshot()
    -            chat_id = next(model.model_id for model in catalog.models if model.model == "chat-a")
    -            revision = (
    -                await settings.apply(
    -                    SetDefaultModel(
    -                        expected_revision=revision,
    -                        role=ModelRole.DEFAULT,
    -                        model_id=chat_id,
    -                    )
    -                )
    -            ).revision
    -            async with root.context.require(CHAT_MODELS).execution() as execution:
    -                chat = execution.chat(ModelRole.DEFAULT)
    -                response = await chat.complete(
    -                    ModelRequest(messages=({"role": "user", "content": "hello"},))
    -                )
    -                assert response.thinking == "checked"
    -                assert response.tool_calls[0].name == "lookup"
    -                deltas: list[dict[str, str]] = []
    -
    -                async def on_delta(delta: dict[str, str]) -> None:
    -                    deltas.append(delta)
    -
    -                streamed = await chat.complete(
    -                    ModelRequest(
    -                        messages=({"role": "user", "content": "stream"},),
    -                        on_delta=on_delta,
    -                    )
    -                )
    -                assert streamed.thinking == "why because"
    -                assert streamed.tool_calls[0].name == "search"
    -                assert streamed.usage is not None
    -                assert streamed.usage.coverage is UsageCoverage.EXACT
    -                assert deltas == [
    -                    {"thinking_delta": "why "},
    -                    {"thinking_delta": "because "},
    -                    {"content_delta": "hello "},
    -                ]
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -            await manager.terminate_all()
    -
    -        registry_path = next((tmp_path / "workspace").rglob("model-registry.sqlite3"))
    -
    -        def write_legacy_tool_limit(value: int) -> None:
    -            connection = sqlite3.connect(registry_path)
    -            try:
    -                row = connection.execute(
    -                    "SELECT capabilities_json FROM model_definitions WHERE model = ?",
    -                    ("chat-a",),
    -                ).fetchone()
    -                assert row is not None
    -                payload = json.loads(row[0])
    -                payload["driver_config"] = {
    -                    "format_version": 1,
    -                    "max_tool_schemas": value,
    -                    "use_responses_lite": False,
    -                    "reasoning_summary": "none",
    -                }
    -                connection.execute(
    -                    "UPDATE model_definitions SET capabilities_json = ? WHERE model = ?",
    -                    (json.dumps(payload), "chat-a"),
    -                )
    -                connection.commit()
    -            finally:
    -                connection.close()
    -
    -        write_legacy_tool_limit(32)
    -        reloaded = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await reloaded.load_all()
    -        snapshot = reloaded.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.AVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG).snapshot().models
    -        )
    -        lease = await reloaded._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            async with snapshot.composition_root.context.require(CHAT_MODELS).execution() as execution:
    -                chat = execution.chat(ModelRole.DEFAULT)
    -                assert chat.max_tool_schemas == 16
    -                response = await chat.complete(
    -                    ModelRequest(messages=({"role": "user", "content": "legacy 32"},))
    -                )
    -                assert response.tool_calls[0].name == "lookup"
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -        await reloaded.terminate_all()
    -
    -        write_legacy_tool_limit(16)
    -        set_installed_plugin_enabled(
    -            "opencode-go@ordinary-test",
    -            enabled=False,
    -            plugins_home=tmp_path / "home",
    -        )
    -        _ = finalize_uninstall_plugin(
    -            "opencode-go@ordinary-test",
    -            workspace=tmp_path / "workspace",
    -            plugins_home=tmp_path / "home",
    -        )
    -        without_driver = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await without_driver.load_all()
    -        snapshot = without_driver.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.DRIVER_UNAVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG).snapshot().models
    -        )
    -        await without_driver.terminate_all()
    -
    -        restored_install = install_git_plugin(
    -            workspace=tmp_path / "workspace",
    -            source=str(repo),
    -            marketplace="ordinary-test",
    -            plugins_home=tmp_path / "home",
    -        )
    -        restored = PluginManager(
    -            plugin_dirs=[],
    -            event_bus=EventBus(),
    -            tool_registry=None,
    -            workspace=tmp_path / "workspace",
    -            installed_cache_root=tmp_path / "home" / "cache",
    -        )
    -        await restored.load_all()
    -        generation = restored.generation("opencode-go@ordinary-test")
    -        assert generation is not None
    -        assert generation.plugin_dir == restored_install.installed_path
    -        snapshot = restored.current_snapshot
    -        assert snapshot is not None and snapshot.composition_root is not None
    -        assert all(
    -            model.availability is ModelAvailability.AVAILABLE
    -            for model in snapshot.composition_root.context.require(MODEL_CATALOG).snapshot().models
    -        )
    -        lease = await restored._snapshot_store.acquire()
    -        token = bind_runtime_snapshot(lease)
    -        try:
    -            async with snapshot.composition_root.context.require(CHAT_MODELS).execution() as execution:
    -                chat = execution.chat(ModelRole.DEFAULT)
    -                assert chat.max_tool_schemas == 16
    -                response = await chat.complete(
    -                    ModelRequest(messages=({"role": "user", "content": "after reinstall"},))
    -                )
    -                assert response.tool_calls[0].name == "lookup"
    -                assert _server.requests[-1]["authorization"] == "Bearer secret"
    -        finally:
    -            reset_runtime_snapshot(token)
    -            await lease.release()
    -        await restored.terminate_all()
    -
    -    assert not any(name.startswith(blocked_prefixes) for name in sys.modules)
    diff --git a/tests/test_peer_agent_tool.py b/tests/test_peer_agent_tool.py
    deleted file mode 100644
    index 95116bc67..000000000
    --- a/tests/test_peer_agent_tool.py
    +++ /dev/null
    @@ -1,29 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.config import load_config
    -
    -
    -def test_legacy_top_level_peer_config_is_explicitly_unsupported(tmp_path: Path) -> None:
    -    config_path = tmp_path / "config.toml"
    -    config_path.write_text("peer_agents = []\n", encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match=r"unsupported capability: peer_agents"):
    -        load_config(config_path, workspace=tmp_path / "workspace")
    -
    -
    -def test_legacy_integrations_peer_config_is_explicitly_unsupported(tmp_path: Path) -> None:
    -    config_path = tmp_path / "config.toml"
    -    config_path.write_text(
    -        "[integrations]\npeer_agents = []\n",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(
    -        ValueError,
    -        match=r"unsupported capability: integrations\.peer_agents",
    -    ):
    -        load_config(config_path, workspace=tmp_path / "workspace")
    diff --git a/tests/test_personamem_eval.py b/tests/test_personamem_eval.py
    deleted file mode 100644
    index f02c58b0d..000000000
    --- a/tests/test_personamem_eval.py
    +++ /dev/null
    @@ -1,117 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -
    -from eval.personamem.dataset import load_dataset
    -from eval.personamem.metrics import extract_option_label, score_results
    -
    -
    -def _write(path: Path, content: str) -> None:
    -    path.write_text(content, encoding="utf-8")
    -
    -
    -def test_personamem_loader_builds_instance(tmp_path: Path) -> None:
    -    questions = tmp_path / "questions.csv"
    -    contexts = tmp_path / "contexts.jsonl"
    -    _write(
    -        questions,
    -        (
    -            "persona_id,question_id,question_type,topic,context_length_in_tokens,"
    -            "context_length_in_letters,distance_to_ref_in_blocks,distance_to_ref_in_tokens,"
    -            "num_irrelevant_tokens,distance_to_ref_proportion_in_context,user_question_or_message,"
    -            "correct_answer,all_options,shared_context_id,end_index_in_shared_context\n"
    -            "7,q1,recall_user_shared_facts,music,10,10,1,1,0,10%,Which one?,(b),"
    -            "\"['(a) First', '(b) Second']\",ctx1,3\n"
    -        ),
    -    )
    -    _write(
    -        contexts,
    -        '{"ctx1": ['
    -        '{"role":"system","content":"Current user persona: Likes music."},'
    -        '{"role":"user","content":"User: hello"},'
    -        '{"role":"assistant","content":"Assistant: hi"},'
    -        '{"role":"user","content":"User: ignored by end index"}'
    -        "]}\n",
    -    )
    -
    -    instances = load_dataset(questions, contexts)
    -
    -    assert len(instances) == 1
    -    inst = instances[0]
    -    assert inst.question_id == "q1"
    -    assert inst.gold_label == "(b)"
    -    assert inst.gold_option == "(b) Second"
    -    assert inst.persona_profile == "Current user persona: Likes music."
    -    assert len(inst.haystack_sessions) == 1
    -    assert inst.haystack_sessions[0][0].role == "user"
    -    assert inst.haystack_sessions[0][0].content == "hello"
    -    assert inst.haystack_sessions[0][1].content == "hi"
    -
    -
    -def test_personamem_loader_splits_turns_into_sessions(tmp_path: Path) -> None:
    -    questions = tmp_path / "questions.csv"
    -    contexts = tmp_path / "contexts.jsonl"
    -    _write(
    -        questions,
    -        (
    -            "persona_id,question_id,question_type,topic,context_length_in_tokens,"
    -            "context_length_in_letters,distance_to_ref_in_blocks,distance_to_ref_in_tokens,"
    -            "num_irrelevant_tokens,distance_to_ref_proportion_in_context,user_question_or_message,"
    -            "correct_answer,all_options,shared_context_id,end_index_in_shared_context\n"
    -            "7,q2,recall_user_shared_facts,music,10,10,1,1,0,10%,Which one?,(a),"
    -            "\"['(a) First', '(b) Second']\",ctx2,5\n"
    -        ),
    -    )
    -    _write(
    -        contexts,
    -        '{"ctx2": ['
    -        '{"role":"user","content":"User: first u"},'
    -        '{"role":"assistant","content":"Assistant: first a"},'
    -        '{"role":"user","content":"User: second u1"},'
    -        '{"role":"user","content":"User: second u2"},'
    -        '{"role":"assistant","content":"Assistant: second a"}'
    -        "]}\n",
    -    )
    -
    -    instances = load_dataset(questions, contexts)
    -
    -    assert len(instances) == 1
    -    inst = instances[0]
    -    assert len(inst.haystack_sessions) == 2
    -    assert [turn.content for turn in inst.haystack_sessions[0]] == ["first u", "first a"]
    -    assert [turn.content for turn in inst.haystack_sessions[1]] == [
    -        "second u1",
    -        "second u2",
    -        "second a",
    -    ]
    -
    -
    -def test_extract_option_label_supports_label_and_text() -> None:
    -    options = ["Alpha", "Bravo choice", "Charlie"]
    -
    -    assert extract_option_label("(b)", options) == "(b)"
    -    assert extract_option_label("I choose b", options) == "(b)"
    -    assert extract_option_label("Bravo choice", options) == "(b)"
    -
    -
    -def test_score_results_returns_accuracy() -> None:
    -    scores = score_results(
    -        [
    -            {
    -                "question_type": "recall_user_shared_facts",
    -                "predicted_label": "(a)",
    -                "is_correct": True,
    -                "error": None,
    -            },
    -            {
    -                "question_type": "recall_user_shared_facts",
    -                "predicted_label": None,
    -                "is_correct": False,
    -                "error": "timeout",
    -            },
    -        ]
    -    )
    -
    -    assert scores["overall"]["accuracy"] == 0.5
    -    assert scores["overall"]["parsed_rate"] == 0.5
    -    assert scores["overall"]["errors"] == 1
    diff --git a/tests/test_plugin_channel_generation_host.py b/tests/test_plugin_channel_generation_host.py
    deleted file mode 100644
    index 9685392d5..000000000
    --- a/tests/test_plugin_channel_generation_host.py
    +++ /dev/null
    @@ -1,2496 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import logging
    -from dataclasses import dataclass
    -from datetime import datetime, timezone
    -from types import ModuleType, SimpleNamespace
    -from typing import Any, Mapping, cast
    -
    -import pytest
    -
    -from agent.plugin_composition.channels import (
    -    AttachmentKind,
    -    AttachmentRef,
    -    AttachmentReadLease,
    -    ChannelCapability,
    -    ChannelCommitRole,
    -    ChannelDeliveryReceipt,
    -    ChannelDefinition,
    -    ChannelFactoryFreezeInput,
    -    ChannelReady,
    -    ChannelTerminalStatus,
    -    ChannelInboundMessage,
    -    CommittedChannelCatalog,
    -    ControlResponseBodies,
    -    CoreChannelDefinition,
    -    InboundIdentity,
    -    CredentialRef,
    -    DeliveryStatus,
    -    InboundEnvelope,
    -    InboundOwner,
    -    OutboundEnvelope,
    -    PluginChannels,
    -    ProviderClient,
    -    ProviderClientFactory,
    -    ProviderDeliveryReceipt,
    -    ProviderDeliveryRequest,
    -    PresentationReceipt,
    -    RawInbound,
    -    StreamDeltaPresentation,
    -    StopReceipt,
    -    TurnStartedPresentation,
    -    TurnStreamEvent,
    -    TurnStreamEventKind,
    -    _freeze_plugin_channels,
    -    channel_config_revision,
    -)
    -from agent.plugin_composition.model import CompositionError, ServiceKey
    -from agent.plugins.channel_generation_host import (
    -    ChannelBindingLease,
    -    ChannelGenerationHost,
    -    ChannelStartRecord,
    -    bind_channel_turn_binding,
    -    get_current_channel_turn_binding,
    -    reset_channel_turn_binding,
    -)
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.generation import GateResult, PluginContributions, PluginGeneration
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler, RuntimeSnapshotLease, RuntimeSnapshotStore
    -from bus.queue import MessageBus
    -from bus.event_bus import EventBus
    -from bus.events_lifecycle import (
    -    StreamDeltaReady,
    -    ToolCallCompleted,
    -    ToolCallStarted,
    -    TurnOutputCompleted,
    -    TurnStarted,
    -)
    -from bootstrap.channel_presentation import ChannelTurnPresentationBridge
    -from session.manager import SessionManager
    -
    -
    -def _diagnostic_fields(record: logging.LogRecord) -> dict[str, object]:
    -    return cast(dict[str, object], getattr(record, "akashic_fields"))
    -
    -
    -@dataclass
    -class ClientFactory:
    -    created: int = 0
    -    closed: int = 0
    -    fail_close: bool = False
    -
    -    async def create(
    -        self,
    -        credentials: Mapping[str, CredentialRef],
    -    ) -> ProviderClient:
    -        self.created += 1
    -        return _Client(credentials)
    -
    -    async def aclose(self) -> None:
    -        self.closed += 1
    -        if self.fail_close:
    -            raise RuntimeError("factory close failed")
    -
    -
    -class _Client:
    -    def __init__(self, credentials: Mapping[str, CredentialRef]) -> None:
    -        self._credentials = credentials
    -
    -    def credential(self, ref: CredentialRef) -> str:
    -        if ref not in self._credentials.values():
    -            raise AssertionError(f"unexpected credential: {ref.path}")
    -        return "test-credential"
    -
    -    async def aclose(self) -> None:
    -        return None
    -
    -
    -class Adapter:
    -    def __init__(
    -        self,
    -        context: Any,
    -        *,
    -        fail_start: bool = False,
    -        fail_stop: bool = False,
    -        block_stop: bool = False,
    -        wrong_receipt: bool = False,
    -        cancel_stop: bool = False,
    -        cancel_start: bool = False,
    -    ) -> None:
    -        self.context = context
    -        self.fail_start = fail_start
    -        self.fail_stop = fail_stop
    -        self.block_stop = block_stop
    -        self.wrong_receipt = wrong_receipt
    -        self.cancel_stop = cancel_stop
    -        self.cancel_start = cancel_start
    -        self.started = 0
    -        self.stopped = 0
    -        self.deliveries: list[str] = []
    -        self.requests: list[ProviderDeliveryRequest] = []
    -        self.release = asyncio.Event()
    -        self.stop_started = asyncio.Event()
    -        self.stop_release = asyncio.Event()
    -        self.runtime_events: list[str] = []
    -
    -    def attach_runtime(self, ports: Any) -> None:
    -        self.runtime_events.append("attach")
    -        self.runtime_ports = ports
    -
    -    def open_admission(self) -> None:
    -        self.runtime_events.append("open")
    -
    -    def close_admission(self) -> None:
    -        self.runtime_events.append("close")
    -
    -    async def start(self) -> ChannelReady:
    -        self.started += 1
    -        if self.cancel_start:
    -            raise asyncio.CancelledError
    -        if self.fail_start:
    -            raise RuntimeError("start failed")
    -        return ChannelReady(self.context.binding_token)
    -
    -    async def deliver(self, request: ProviderDeliveryRequest) -> ProviderDeliveryReceipt:
    -        self.deliveries.append(request.delivery_id)
    -        self.requests.append(request)
    -        if not self.release.is_set():
    -            await self.release.wait()
    -        delivery_id = "wrong" if self.wrong_receipt else request.delivery_id
    -        return ProviderDeliveryReceipt(delivery_id, DeliveryStatus.DELIVERED, ("p1",))
    -
    -    async def stop(self) -> StopReceipt:
    -        self.stopped += 1
    -        self.stop_started.set()
    -        if self.block_stop:
    -            await self.stop_release.wait()
    -        if self.cancel_stop:
    -            raise asyncio.CancelledError
    -        if self.fail_stop:
    -            raise RuntimeError("stop failed")
    -        return StopReceipt(self.context.binding_token, True)
    -
    -
    -class PresentationAdapter(Adapter):
    -    def attach_presentation(self, ports: Any) -> None:
    -        self.presentation_ports = ports
    -
    -
    -class IncompleteStopAdapter(Adapter):
    -    def __init__(self, context: Any, **kwargs: Any) -> None:
    -        super().__init__(context, **kwargs)
    -        self.resources_closed = False
    -
    -    async def stop(self) -> StopReceipt:
    -        self.stopped += 1
    -        self.stop_started.set()
    -        return StopReceipt(self.context.binding_token, self.resources_closed)
    -
    -
    -async def _noop_record(record: ChannelStartRecord) -> None:
    -    return None
    -
    -
    -async def _noop_failure(failure: Any) -> None:
    -    return None
    -
    -
    -def _host(**kwargs: Any) -> ChannelGenerationHost:
    -    kwargs.setdefault("on_before_start", _noop_record)
    -    kwargs.setdefault("config_revision_checker", _noop_record)
    -    kwargs.setdefault("on_failure", _noop_failure)
    -    return ChannelGenerationHost(**kwargs)
    -
    -
    -class _FakeSnapshotLease(RuntimeSnapshotLease):
    -    def __init__(
    -        self,
    -        snapshot: Any,
    -        *,
    -        release_gate: asyncio.Event | None = None,
    -    ) -> None:
    -        self.snapshot = snapshot
    -        self._active = True
    -        self.release_gate = release_gate
    -        self.forks: list[_FakeSnapshotLease] = []
    -
    -    @property
    -    def active(self) -> bool:
    -        return self._active
    -
    -    def fork(self) -> _FakeSnapshotLease:
    -        if not self.active:
    -            raise RuntimeError("lease closed")
    -        child = _FakeSnapshotLease(
    -            self.snapshot,
    -            release_gate=self.release_gate,
    -        )
    -        self.forks.append(child)
    -        return child
    -
    -    async def release(self) -> None:
    -        if not self.active:
    -            return
    -        if self.release_gate is not None:
    -            await self.release_gate.wait()
    -        self._active = False
    -
    -
    -def _attachment_ref() -> AttachmentRef:
    -    return AttachmentRef(
    -        artifact_id="artifact-1",
    -        kind=AttachmentKind.FILE,
    -        filename="report.txt",
    -        media_type="text/plain",
    -        size_bytes=5,
    -        sha256="a" * 64,
    -    )
    -
    -
    -class _FakeAttachmentReadLease:
    -    def __init__(
    -        self,
    -        ref: AttachmentRef,
    -        *,
    -        close_started: asyncio.Event | None = None,
    -        close_release: asyncio.Event | None = None,
    -    ) -> None:
    -        self.ref = ref
    -        self.close_started = close_started
    -        self.close_release = close_release
    -        self.close_calls = 0
    -
    -    async def read_bytes(self, *, max_bytes: int) -> bytes:
    -        assert max_bytes >= 5
    -        return b"hello"
    -
    -    async def aclose(self) -> None:
    -        self.close_calls += 1
    -        if self.close_started is not None:
    -            self.close_started.set()
    -        if self.close_release is not None:
    -            await self.close_release.wait()
    -
    -
    -class _FakeAttachmentImportPort:
    -    def __init__(self, ref: AttachmentRef) -> None:
    -        self.ref = ref
    -        self.calls = 0
    -        self.fail = False
    -        self.gate: asyncio.Event | None = None
    -
    -    async def import_bytes(
    -        self,
    -        data: bytes,
    -        *,
    -        kind: AttachmentKind,
    -        filename: str | None,
    -        media_type: str | None,
    -    ) -> AttachmentRef:
    -        self.calls += 1
    -        assert data == b"hello"
    -        assert kind is AttachmentKind.FILE
    -        assert filename == "report.txt"
    -        assert media_type == "text/plain"
    -        if self.fail:
    -            raise OSError("import failed")
    -        if self.gate is not None:
    -            await self.gate.wait()
    -        return self.ref
    -
    -
    -class _FakeAttachmentReadPort:
    -    def __init__(self, lease: AttachmentReadLease) -> None:
    -        self.lease = lease
    -        self.calls = 0
    -        self.fail = False
    -        self.gate: asyncio.Event | None = None
    -
    -    async def acquire(self, ref: AttachmentRef) -> AttachmentReadLease:
    -        self.calls += 1
    -        assert ref == self.lease.ref
    -        if self.fail:
    -            raise OSError("acquire failed")
    -        if self.gate is not None:
    -            await self.gate.wait()
    -        return self.lease
    -
    -
    -def _module(
    -    *,
    -    name: str = "feishu",
    -    factory_name: str = "make_adapter",
    -    adapter_cls: type[Adapter] = Adapter,
    -) -> ModuleType:
    -    module = ModuleType(f"plugins.{name}")
    -    module.api_version = 3  # type: ignore[attr-defined]
    -    module.name = name  # type: ignore[attr-defined]
    -    module.version = "1"  # type: ignore[attr-defined]
    -    module.inject = (ServiceKey("core.channels"),)  # type: ignore[attr-defined]
    -    async def apply(ctx: Any, config: Any) -> None:
    -        return None
    -
    -    module.apply = apply  # type: ignore[attr-defined]
    -    setattr(module, factory_name, lambda context: adapter_cls(context))
    -    return module
    -
    -
    -async def _make_snapshot(
    -    *,
    -    module: ModuleType | None = None,
    -    adapter_cls: type[Adapter] = Adapter,
    -    fail_start: bool = False,
    -    fail_stop: bool = False,
    -    block_stop: bool = False,
    -    wrong_receipt: bool = False,
    -    cancel_stop: bool = False,
    -    cancel_start: bool = False,
    -    cancel_factory: bool = False,
    -    fail_after: int | None = None,
    -    factory_events: list[str] | None = None,
    -    capabilities: frozenset[ChannelCapability] = frozenset(
    -        {ChannelCapability.OUTBOUND}
    -    ),
    -) -> tuple[Any, dict[str, ClientFactory], dict[str, Adapter]]:
    -    module = module or _module(adapter_cls=adapter_cls)
    -    plugin = ComposablePlugin.from_module(module)
    -    root_token = object()
    -    channels = PluginChannels(root_token)
    -    from agent.plugin_composition.channels import CredentialRef
    -
    -    class Fiber:
    -        activation_token = object()
    -
    -    class Runtime:
    -        plugin_id = "plugin.feishu"
    -        config = {"app_secret": CredentialRef(("app_secret",))}
    -
    -    class Context:
    -        fiber = Fiber()
    -        runtime = Runtime()
    -        generation_id = "gen-1"
    -
    -        def report_incident(self, *args: Any) -> Any:
    -            return None
    -
    -        def require(self, key: Any) -> Any:
    -            return channels
    -
    -        def _root_instance_token(self) -> object:
    -            return root_token
    -
    -        async def effect(self, setup: Any, label: str) -> Any:
    -            setup()
    -            return SimpleNamespace(aclose=lambda: None)
    -
    -        async def health(self, *args: Any, **kwargs: Any) -> Any:
    -            return SimpleNamespace()
    -
    -    channel_names = tuple(getattr(module, "channel_names", ("feishu",)))
    -    config_projection: dict[str, object] = {
    -        "app_secret": CredentialRef(("app_secret",))
    -    }
    -    for channel_name in channel_names:
    -        await channels.register(
    -            cast(Any, Context()),
    -            ChannelDefinition(
    -                name=channel_name,
    -                capabilities=capabilities,
    -                factory_export="make_adapter",
    -                inbound_identity=(
    -                    InboundIdentity.PROVIDER_MESSAGE_ID
    -                    if ChannelCapability.INBOUND in capabilities
    -                    else None
    -                ),
    -                credential_paths=("app_secret",),
    -            ),
    -        )
    -    registry = _freeze_plugin_channels(
    -        channels,
    -        root_token,
    -        factory_provenance_by_owner={
    -            "plugin.feishu": ChannelFactoryFreezeInput(
    -                "gen-1",
    -                "source-1",
    -                channel_config_revision(config_projection),
    -            )
    -        },
    -    )
    -    generation = PluginGeneration(
    -        plugin_id="plugin.feishu",
    -        generation_id="gen-1",
    -        module_path="plugins/feishu/plugin.py",
    -        source_revision="source-1",
    -        config_revision="raw-config-1",
    -        plugin_dir=__import__("pathlib").Path("/tmp/plugin"),
    -        data_dir=__import__("pathlib").Path("/tmp/plugin-data"),
    -        config={"app_secret": "raw-secret"},
    -        instance=plugin,
    -        scope=cast(Any, object()),
    -        contributions=PluginContributions(manifest={}),
    -        gate_result=GateResult("test", "plugin.feishu", "rev", "passed", ()),
    -        config_projection=config_projection,
    -    )
    -    snapshot = SimpleNamespace(
    -        snapshot_id="snapshot-1",
    -        state="committed",
    -        composition_root=SimpleNamespace(instance_token=root_token),
    -        channel_registry=registry,
    -        channel_registry_identity=registry.identity,
    -        generations={"plugin.feishu": generation},
    -    )
    -    adapters: dict[str, Adapter] = {}
    -    factory_count = 0
    -
    -    def factory(context: Any) -> Adapter:
    -        nonlocal factory_count
    -        factory_count += 1
    -        if factory_events is not None:
    -            factory_events.append("factory")
    -        if cancel_factory:
    -            raise asyncio.CancelledError
    -        adapter = adapter_cls(
    -            context,
    -            fail_start=fail_start or (fail_after is not None and factory_count >= fail_after),
    -            fail_stop=fail_stop,
    -            block_stop=block_stop,
    -            wrong_receipt=wrong_receipt,
    -            cancel_stop=cancel_stop,
    -            cancel_start=cancel_start,
    -        )
    -        adapters[context.binding_token] = adapter
    -        return adapter
    -
    -    setattr(module, "make_adapter", factory)
    -    return snapshot, {channel_name: ClientFactory() for channel_name in channel_names}, adapters
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_binding_starts_closed_and_delivers_after_open() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    binding = generation.channel("feishu")
    -    assert not binding.admission_open
    -    with pytest.raises(RuntimeError, match="关闭"):
    -        await binding.deliver(ProviderDeliveryRequest(binding.binding_token, "d1", "u", "hi"))
    -    binding.open_admission()
    -    request = ProviderDeliveryRequest(binding.binding_token, "d1", "u", "hi")
    -    task = asyncio.create_task(binding.deliver(request))
    -    await asyncio.sleep(0)
    -    assert binding.in_flight == 1
    -    binding.close_admission()
    -    assert binding.in_flight == 1
    -    next_request = ProviderDeliveryRequest(binding.binding_token, "d2", "u", "hi")
    -    with pytest.raises(RuntimeError, match="关闭"):
    -        await binding.deliver(next_request)
    -    for adapter in adapters.values():
    -        adapter.release.set()
    -    receipt = await task
    -    assert receipt.delivery_id == "d1"
    -    await generation.stop()
    -    assert factories["feishu"].closed == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_channel_callbacks_share_generic_diagnostic_boundary(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    binding = generation.channel("feishu")
    -    binding.open_admission()
    -    adapters[binding.binding_token].release.set()
    -    _ = await binding.deliver(
    -        ProviderDeliveryRequest(binding.binding_token, "d1", "u", "hi")
    -    )
    -    binding.close_admission()
    -    _ = await generation.stop()
    -
    -    terminals = [
    -        _diagnostic_fields(record)
    -        for record in caplog.records
    -        if _diagnostic_fields(record).get("event") == "plugin.operation.done"
    -    ]
    -    assert {item["operation"] for item in terminals} == {
    -        "channel.factory",
    -        "channel.attach_runtime",
    -        "channel.attach_presentation",
    -        "channel.start",
    -        "channel.open_admission",
    -        "channel.deliver",
    -        "channel.close_admission",
    -        "channel.stop",
    -    }
    -    assert all(item["plugin_id"] == "plugin.feishu" for item in terminals)
    -    assert all(item["generation_id"] == "gen-1" for item in terminals)
    -    assert all(item["plugin_entrypoint"] == "feishu" for item in terminals)
    -
    -
    -@pytest.mark.asyncio
    -async def test_inbound_runtime_attaches_closed_then_opens_and_closes_before_drain() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset({ChannelCapability.INBOUND})
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    adapter = adapters[next(iter(adapters))]
    -    assert adapter.runtime_events == ["attach"]
    -
    -    binding = generation.channel("feishu")
    -    assert not binding.admission_open
    -    binding.open_admission()
    -    assert adapter.runtime_events == ["attach", "open"]
    -    binding.close_admission()
    -    assert adapter.runtime_events == ["attach", "open", "close"]
    -
    -    await generation.stop()
    -    assert adapter.runtime_events == ["attach", "open", "close"]
    -    assert factories["feishu"].closed == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_control_uses_exact_binding_and_bounded_dedupe() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    sources: list[_FakeSnapshotLease] = []
    -    requested_snapshot_ids: list[str] = []
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        requested_snapshot_ids.append(snapshot_id)
    -        source = _FakeSnapshotLease(snapshot)
    -        sources.append(source)
    -        return source
    -
    -    interrupted: list[str] = []
    -    dispatched: list[tuple[str, str]] = []
    -
    -    async def interrupt(raw: RawInbound) -> bool:
    -        interrupted.append(raw.message_id)
    -        return raw.message_id != "stop-idle"
    -
    -    async def dispatch(envelope: OutboundEnvelope, binding: Any) -> ChannelDeliveryReceipt:
    -        dispatched.append((envelope.binding_token, binding.binding_token))
    -        return ChannelDeliveryReceipt(envelope.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        control_interrupter=interrupt,
    -        control_response_dispatcher=dispatch,
    -    )
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    ports = adapter.presentation_ports
    -    assert ports.control is not None
    -    assert ports.turn_stream is not None
    -    raw = RawInbound(
    -        message_id="stop-1",
    -        provider_identity="account-1",
    -        recipient="chat-1",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="user",
    -            chat_id="chat-1",
    -            content="/stop",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    first = await ports.control.interrupt(
    -        raw,
    -        response_bodies=ControlResponseBodies("interrupted", "idle"),
    -    )
    -    duplicate = await ports.control.interrupt(
    -        raw,
    -        response_bodies=ControlResponseBodies("interrupted", "idle"),
    -    )
    -    assert first.accepted is True
    -    assert first.reason == "interrupted"
    -    assert first.response is not None
    -    assert duplicate.accepted is False and duplicate.reason == "duplicate"
    -    assert interrupted == ["stop-1"]
    -    assert len(dispatched) == 1
    -    assert dispatched[0][0] == dispatched[0][1]
    -    assert len(sources) == 1
    -    assert not sources[0].active and not sources[0].forks[0].active
    -    idle = RawInbound(
    -        message_id="stop-idle",
    -        provider_identity="account-1",
    -        recipient="chat-1",
    -        message=raw.message,
    -    )
    -    idle_receipt = await ports.control.interrupt(
    -        idle,
    -        response_bodies=ControlResponseBodies("interrupted", "idle"),
    -    )
    -    assert idle_receipt.accepted is False and idle_receipt.reason == "idle"
    -    assert idle_receipt.response is not None
    -    assert requested_snapshot_ids == [snapshot.snapshot_id, snapshot.snapshot_id]
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_control_cancelled_during_source_release_closes_binding() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    release_gate = asyncio.Event()
    -    sources: list[_FakeSnapshotLease] = []
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        source = _FakeSnapshotLease(snapshot, release_gate=release_gate)
    -        sources.append(source)
    -        return source
    -
    -    host = _host(snapshot_lease_acquirer=acquire)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    control = tuple(adapters.values())[0].presentation_ports.control
    -    assert control is not None
    -    task = asyncio.create_task(
    -        control.interrupt(
    -            RawInbound(
    -                message_id="cancel-source-release",
    -                message=ChannelInboundMessage(
    -                    channel="feishu",
    -                    sender="sender",
    -                    chat_id="chat",
    -                    content="/stop",
    -                    timestamp=datetime.now(timezone.utc),
    -                    metadata={},
    -                ),
    -            ),
    -            response_bodies=ControlResponseBodies("stopped", "idle"),
    -        )
    -    )
    -    while not sources or not sources[0].forks:
    -        await asyncio.sleep(0)
    -
    -    task.cancel()
    -    await asyncio.sleep(0)
    -    assert not task.done()
    -    release_gate.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await task
    -
    -    assert not sources[0].active
    -    assert not sources[0].forks[0].active
    -    assert generation.channel("feishu").in_flight == 0
    -    assert not host._binding_leases
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_existing_turn_lease_can_finish_stream_after_close_admission() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host(snapshot_lease_acquirer=lambda snapshot_id: _lease_for(snapshot))
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    events: list[str] = []
    -    callback_started = asyncio.Event()
    -    callback_release = asyncio.Event()
    -
    -    async def callback(event: TurnStreamEvent) -> PresentationReceipt:
    -        events.append(event.kind.value)
    -        if event.kind is TurnStreamEventKind.STREAM_DELTA:
    -            callback_started.set()
    -            await callback_release.wait()
    -        return PresentationReceipt(event.presentation_id, DeliveryStatus.DELIVERED)
    -
    -    ports = adapter.presentation_ports
    -    assert ports.turn_stream is not None
    -    subscription = ports.turn_stream.subscribe(callback)
    -    binding = generation.channel("feishu")
    -    source = _FakeSnapshotLease(snapshot)
    -    lease = host.acquire_binding(source, "feishu")
    -    await binding.publish_turn_event(
    -        TurnStreamEvent(
    -            "preview-1",
    -            TurnStreamEventKind.TURN_STARTED,
    -            TurnStartedPresentation("turn-1", "client-1"),
    -        )
    -    )
    -    binding.close_admission()
    -    blocked = asyncio.create_task(
    -        lease.publish_turn_event(
    -            TurnStreamEvent(
    -                "preview-1",
    -                TurnStreamEventKind.STREAM_DELTA,
    -                StreamDeltaPresentation("turn-1", 1, "hello", ""),
    -            )
    -        )
    -    )
    -    await callback_started.wait()
    -    stop = asyncio.create_task(generation.stop())
    -    await asyncio.sleep(0)
    -    assert not stop.done()
    -    callback_release.set()
    -    assert len(await blocked) == 1
    -    await lease.aclose()
    -    await stop
    -    await subscription.close()
    -    assert events == ["turn.started", "stream.delta"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_control_claim_blocks_old_binding_drain_before_lease_acquire() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    interrupt_started = asyncio.Event()
    -    interrupt_release = asyncio.Event()
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        return _FakeSnapshotLease(snapshot)
    -
    -    async def interrupt(_raw: RawInbound) -> str:
    -        interrupt_started.set()
    -        await interrupt_release.wait()
    -        return "interrupted"
    -
    -    async def dispatch(
    -        envelope: OutboundEnvelope,
    -        _binding: object,
    -    ) -> ChannelDeliveryReceipt:
    -        return ChannelDeliveryReceipt(envelope.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        control_interrupter=interrupt,
    -        control_response_dispatcher=dispatch,
    -    )
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    control = tuple(adapters.values())[0].presentation_ports.control
    -    assert control is not None
    -    raw = RawInbound(
    -        message_id="control-race",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="sender",
    -            chat_id="chat",
    -            content="/stop",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -    task = asyncio.create_task(
    -        control.interrupt(
    -            raw,
    -            response_bodies=ControlResponseBodies("stopped", "idle"),
    -        )
    -    )
    -    await interrupt_started.wait()
    -    generation.channel("feishu").close_admission()
    -    drain = asyncio.create_task(generation.channel("feishu").drain())
    -    await asyncio.sleep(0)
    -    assert not drain.done()
    -    interrupt_release.set()
    -    assert (await task).accepted is True
    -    await drain
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_control_claim_survives_real_store_provisional_pause() -> None:
    -    prototype, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    compiler = RuntimeSnapshotCompiler()
    -    stable = compiler.compile(prototype.generations, snapshot_revision="stable")
    -    latest = compiler.compile(prototype.generations, snapshot_revision="latest")
    -    store = RuntimeSnapshotStore()
    -    store.install(stable)
    -    stable.composition_root = prototype.composition_root
    -    stable.channel_registry = prototype.channel_registry
    -    stable.channel_registry_identity = prototype.channel_registry_identity
    -    interrupted = asyncio.Event()
    -    release = asyncio.Event()
    -
    -    async def interrupt(_raw: RawInbound) -> str:
    -        interrupted.set()
    -        await release.wait()
    -        return "interrupted"
    -
    -    async def dispatch(
    -        envelope: OutboundEnvelope,
    -        _binding: object,
    -    ) -> ChannelDeliveryReceipt:
    -        return ChannelDeliveryReceipt(envelope.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    host = _host(
    -        snapshot_lease_acquirer=store.lease,
    -        control_interrupter=interrupt,
    -        control_response_dispatcher=dispatch,
    -    )
    -    generation = await host.start_formal(stable, factories)
    -    generation.open_admission()
    -    control = tuple(adapters.values())[0].presentation_ports.control
    -    assert control is not None
    -    task = asyncio.create_task(
    -        control.interrupt(
    -            RawInbound(
    -                message_id="control-provisional",
    -                message=ChannelInboundMessage(
    -                    channel="feishu",
    -                    sender="sender",
    -                    chat_id="chat",
    -                    content="/stop",
    -                    timestamp=datetime.now(timezone.utc),
    -                    metadata={},
    -                ),
    -            ),
    -            response_bodies=ControlResponseBodies("stopped", "idle"),
    -        )
    -    )
    -    await interrupted.wait()
    -    transaction = store.begin_publish(latest)
    -    await store.commit_provisional(transaction)
    -    generation.channel("feishu").close_admission()
    -    drain = asyncio.create_task(generation.channel("feishu").drain())
    -    await asyncio.sleep(0)
    -    assert not drain.done()
    -    release.set()
    -    assert (await task).accepted is True
    -    await drain
    -    await generation.stop()
    -    await store.rollback_provisional(transaction, keep_candidate_latest=False)
    -    await store.abort(transaction)
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_production_bridge_preserves_old_binding_and_typed_sequence() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    received: list[TurnStreamEvent] = []
    -
    -    async def callback(event: TurnStreamEvent) -> PresentationReceipt:
    -        received.append(event)
    -        return PresentationReceipt(event.presentation_id, DeliveryStatus.DELIVERED)
    -
    -    assert adapter.presentation_ports.turn_stream is not None
    -    adapter.presentation_ports.turn_stream.subscribe(callback)
    -    source = _FakeSnapshotLease(snapshot)
    -    lease = host.acquire_binding(source, "feishu")
    -    bus = EventBus()
    -    bridge = ChannelTurnPresentationBridge(bus)
    -    token = bind_channel_turn_binding(lease)
    -    try:
    -        await bus.observe(
    -            TurnStarted(
    -                session_key="feishu:chat",
    -                channel="feishu",
    -                chat_id="chat",
    -                content="hello",
    -                timestamp=datetime.now(timezone.utc),
    -                turn_id="turn-bridge",
    -                client_message_id="provider-message",
    -            )
    -        )
    -        generation.channel("feishu").close_admission()
    -        await bus.observe(
    -            StreamDeltaReady(
    -                "feishu:chat",
    -                "feishu",
    -                "chat",
    -                "turn-bridge",
    -                "delta",
    -                "thinking",
    -            )
    -        )
    -        await bus.observe(
    -            ToolCallStarted(
    -                "feishu:chat",
    -                "feishu",
    -                "chat",
    -                1,
    -                "call-1",
    -                "search",
    -                {},
    -                "turn-bridge",
    -            )
    -        )
    -        await bus.observe(
    -            ToolCallCompleted(
    -                "feishu:chat",
    -                "feishu",
    -                "chat",
    -                1,
    -                "call-1",
    -                "search",
    -                {},
    -                {},
    -                "success",
    -                "ok",
    -                {},
    -                "turn-bridge",
    -            )
    -        )
    -        await bus.observe(
    -            TurnOutputCompleted(
    -                "feishu:chat",
    -                "feishu",
    -                "chat",
    -                "turn-bridge",
    -                "provider-message",
    -            )
    -        )
    -    finally:
    -        reset_channel_turn_binding(token)
    -        await bridge.aclose()
    -        await lease.aclose()
    -        await generation.stop()
    -
    -    assert [event.kind for event in received] == list(TurnStreamEventKind)
    -    assert received[0].payload.client_message_id == "provider-message"
    -    assert [event.payload.sequence for event in received[1:]] == [1, 2, 3, 4]
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_turn_binding_does_not_leak_into_child_task() -> None:
    -    snapshot, factories, _adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    lease = host.acquire_binding(_FakeSnapshotLease(snapshot), "feishu")
    -    token = bind_channel_turn_binding(lease)
    -    try:
    -        assert get_current_channel_turn_binding() is lease
    -        async def inherited_binding() -> object:
    -            return get_current_channel_turn_binding()
    -
    -        child = asyncio.create_task(inherited_binding())
    -        assert await child is None
    -    finally:
    -        reset_channel_turn_binding(token)
    -        await lease.aclose()
    -        await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_callback_failure_settles_unknown_and_stops_presentation(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    ports = adapter.presentation_ports
    -    assert ports.turn_stream is not None
    -
    -    async def broken(_event: TurnStreamEvent) -> PresentationReceipt:
    -        raise RuntimeError("provider after-effect failure")
    -
    -    ports.turn_stream.subscribe(broken)
    -    event = TurnStreamEvent(
    -        "preview-failure",
    -        TurnStreamEventKind.TURN_STARTED,
    -        TurnStartedPresentation("turn-failure", "client-failure"),
    -    )
    -    receipts = await generation.channel("feishu").publish_turn_event(event)
    -    assert receipts[0].status is DeliveryStatus.UNKNOWN
    -    with pytest.raises(RuntimeError, match="已因 UNKNOWN 终止"):
    -        await generation.channel("feishu").publish_turn_event(
    -            TurnStreamEvent(
    -                "preview-failure",
    -                TurnStreamEventKind.STREAM_DELTA,
    -                StreamDeltaPresentation("turn-failure", 1, "x", ""),
    -            )
    -        )
    -    await generation.stop()
    -    terminal = next(
    -        _diagnostic_fields(record)
    -        for record in caplog.records
    -        if _diagnostic_fields(record).get("event") == "plugin.operation.error"
    -        and _diagnostic_fields(record).get("operation") == "channel.turn_stream"
    -    )
    -    assert terminal["generation_id"] == "gen-1"
    -    assert terminal["error_type"] == "RuntimeError"
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_callback_contract_mismatch_settles_unknown_before_raise() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    ports = adapter.presentation_ports
    -    assert ports.turn_stream is not None
    -
    -    async def wrong_receipt(_event: TurnStreamEvent) -> PresentationReceipt:
    -        return PresentationReceipt("wrong-presentation", DeliveryStatus.DELIVERED)
    -
    -    ports.turn_stream.subscribe(wrong_receipt)
    -    with pytest.raises(TypeError, match="identity 不匹配"):
    -        await generation.channel("feishu").publish_turn_event(
    -            TurnStreamEvent(
    -                "preview-mismatch",
    -                TurnStreamEventKind.TURN_STARTED,
    -                TurnStartedPresentation("turn-mismatch", "client-mismatch"),
    -            )
    -        )
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_callback_cancellation_waits_for_terminal_cleanup() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset(ChannelCapability),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    ports = adapter.presentation_ports
    -    assert ports.turn_stream is not None
    -    started = asyncio.Event()
    -    release = asyncio.Event()
    -
    -    async def slow(_event: TurnStreamEvent) -> PresentationReceipt:
    -        started.set()
    -        await release.wait()
    -        return PresentationReceipt("preview-cancel", DeliveryStatus.DELIVERED)
    -
    -    ports.turn_stream.subscribe(slow)
    -    task = asyncio.create_task(
    -        generation.channel("feishu").publish_turn_event(
    -            TurnStreamEvent(
    -                "preview-cancel",
    -                TurnStreamEventKind.TURN_STARTED,
    -                TurnStartedPresentation("turn-cancel", "client-cancel"),
    -            )
    -        )
    -    )
    -    await started.wait()
    -    task.cancel()
    -    await asyncio.sleep(0)
    -    assert not task.done()
    -    release.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await task
    -    assert generation.channel("feishu").in_flight == 0
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_c14d_ports_are_capability_gated_per_binding() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=PresentationAdapter,
    -        capabilities=frozenset({ChannelCapability.OUTBOUND, ChannelCapability.CONTROL}),
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    adapter = tuple(adapters.values())[0]
    -    ports = adapter.presentation_ports
    -    assert ports.control is not None
    -    assert ports.turn_stream is None
    -    assert adapter.context.control is ports.control
    -    assert adapter.context.turn_stream is None
    -    generation.open_admission()
    -    with pytest.raises(RuntimeError, match="exact snapshot lease"):
    -        await ports.control.interrupt(
    -            RawInbound(
    -                message_id="control-no-lease",
    -                message=ChannelInboundMessage(
    -                    channel="feishu",
    -                    sender="sender",
    -                    chat_id="chat",
    -                    content="/stop",
    -                    timestamp=datetime.now(timezone.utc),
    -                    metadata={},
    -                ),
    -            ),
    -            response_bodies=ControlResponseBodies("interrupted", "idle"),
    -        )
    -    await generation.stop()
    -
    -
    -async def _lease_for(snapshot: Any) -> _FakeSnapshotLease:
    -    return _FakeSnapshotLease(snapshot)
    -
    -
    -@pytest.mark.asyncio
    -async def test_exact_binding_lease_blocks_stop_until_snapshot_fork_closes() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    source = _FakeSnapshotLease(snapshot)
    -
    -    owner = host.acquire_binding(cast(Any, source), "feishu")
    -    assert owner.snapshot_id == snapshot.snapshot_id
    -    assert owner.generation_id == "gen-1"
    -    assert owner.channel_name == "feishu"
    -    assert owner.active
    -    stop = asyncio.create_task(generation.stop())
    -    await asyncio.sleep(0)
    -    assert not stop.done()
    -
    -    await owner.aclose()
    -    assert not owner.active
    -    assert source.active
    -    assert len(source.forks) == 1 and not source.forks[0].active
    -    await stop
    -
    -
    -@pytest.mark.asyncio
    -async def test_exact_binding_lease_dispatches_one_outbound_envelope() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    source = _FakeSnapshotLease(snapshot)
    -    owner = host.acquire_binding(cast(Any, source), "feishu")
    -    envelope = OutboundEnvelope(
    -        logical_delivery_id="d1",
    -        delivery_id="d1",
    -        attempt_sequence=1,
    -        snapshot_id=snapshot.snapshot_id,
    -        generation_id="gen-1",
    -        binding_token=owner.binding_token,
    -        channel="feishu",
    -        recipient="u",
    -        body="hi",
    -        metadata={},
    -        commit_role=ChannelCommitRole.PASSIVE,
    -        thinking="thinking",
    -        reply_to="reply",
    -        session_message_id="message",
    -        control_turn_id="turn",
    -        execution_attempt_id="attempt",
    -        terminal_status=ChannelTerminalStatus.COMPLETED,
    -    )
    -    for adapter in adapters.values():
    -        adapter.release.set()
    -
    -    receipt = await host.dispatch_outbound(envelope, owner)
    -
    -    assert receipt == ChannelDeliveryReceipt(
    -        "d1",
    -        DeliveryStatus.DELIVERED,
    -        ("p1",),
    -    )
    -    assert tuple(adapters.values())[0].deliveries == ["d1"]
    -    request = tuple(adapters.values())[0].requests[0]
    -    assert request.commit_role is ChannelCommitRole.PASSIVE
    -    assert request.thinking == "thinking"
    -    assert request.reply_to == "reply"
    -    assert request.session_message_id == "message"
    -    assert request.control_turn_id == "turn"
    -    assert request.execution_attempt_id == "attempt"
    -    assert request.terminal_status is ChannelTerminalStatus.COMPLETED
    -    await owner.aclose()
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_exact_binding_lease_finishes_delivery_after_admission_closes() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    owner = host.acquire_binding(cast(Any, _FakeSnapshotLease(snapshot)), "feishu")
    -    envelope = OutboundEnvelope(
    -        logical_delivery_id="d1",
    -        delivery_id="d1",
    -        attempt_sequence=1,
    -        snapshot_id=snapshot.snapshot_id,
    -        generation_id="gen-1",
    -        binding_token=owner.binding_token,
    -        channel="feishu",
    -        recipient="u",
    -        body="hi",
    -        metadata={},
    -    )
    -
    -    generation.close_admission()
    -    direct = generation.channel("feishu")
    -    with pytest.raises(RuntimeError, match="关闭"):
    -        await direct.deliver(
    -            ProviderDeliveryRequest(direct.binding_token, "new", "u", "hi")
    -        )
    -    delivery = asyncio.create_task(host.dispatch_outbound(envelope, owner))
    -    await asyncio.sleep(0)
    -    assert tuple(adapters.values())[0].deliveries == ["d1"]
    -    await owner.aclose()
    -    draining = asyncio.create_task(generation.drain())
    -    await asyncio.sleep(0)
    -    assert not draining.done()
    -    tuple(adapters.values())[0].release.set()
    -    receipt = await delivery
    -    await draining
    -
    -    assert receipt.status is DeliveryStatus.DELIVERED
    -    assert tuple(adapters.values())[0].deliveries == ["d1"]
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_retained_delivery_rejects_forged_or_stopping_binding() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    source = _FakeSnapshotLease(snapshot)
    -    owner = host.acquire_binding(cast(Any, source), "feishu")
    -    envelope = OutboundEnvelope(
    -        logical_delivery_id="d1",
    -        delivery_id="d1",
    -        attempt_sequence=1,
    -        snapshot_id=snapshot.snapshot_id,
    -        generation_id="gen-1",
    -        binding_token=owner.binding_token,
    -        channel="feishu",
    -        recipient="u",
    -        body="hi",
    -        metadata={},
    -    )
    -    forged = ChannelBindingLease(
    -        host,
    -        (snapshot.snapshot_id, "feishu"),
    -        _FakeSnapshotLease(snapshot),
    -    )
    -    generation.close_admission()
    -    with pytest.raises(RuntimeError, match="Host 登记"):
    -        await host.dispatch_outbound(envelope, forged)
    -    assert tuple(adapters.values())[0].deliveries == []
    -
    -    stopping = asyncio.create_task(generation.stop())
    -    for _ in range(20):
    -        if host._binding((snapshot.snapshot_id, "feishu")).stopping:
    -            break
    -        await asyncio.sleep(0)
    -    assert host._binding((snapshot.snapshot_id, "feishu")).stopping
    -    assert not stopping.done()
    -    with pytest.raises(RuntimeError, match="关闭"):
    -        await host.dispatch_outbound(envelope, owner)
    -    await owner.aclose()
    -    await stopping
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_dispatch_rejects_foreign_host_binding() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    owner = host.acquire_binding(cast(Any, _FakeSnapshotLease(snapshot)), "feishu")
    -    envelope = OutboundEnvelope(
    -        logical_delivery_id="d1",
    -        delivery_id="d1",
    -        attempt_sequence=1,
    -        snapshot_id=snapshot.snapshot_id,
    -        generation_id="gen-1",
    -        binding_token=owner.binding_token,
    -        channel="feishu",
    -        recipient="u",
    -        body="hi",
    -        metadata={},
    -    )
    -
    -    with pytest.raises(RuntimeError, match="不属于当前 Host"):
    -        await _host().dispatch_outbound(envelope, owner)
    -
    -    await owner.aclose()
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_ingress_acquires_exact_binding_and_deduplicates() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    sources: list[_FakeSnapshotLease] = []
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        source = _FakeSnapshotLease(snapshot)
    -        sources.append(source)
    -        return source
    -
    -    bus = MessageBus()
    -    host = _host(snapshot_lease_acquirer=acquire)
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    assert adapter.context.ingress is not None
    -    assert not hasattr(adapter.context, "recovery_ingress")
    -    assert adapter.runtime_ports.recovery_ingress is None
    -    raw = RawInbound(
    -        message_id="provider-message-1",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="user",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    assert await adapter.context.ingress.admit(raw) is True
    -    assert await adapter.context.ingress.admit(raw) is False
    -    assert len(sources) == 1 and not sources[0].active
    -    assert len(sources[0].forks) == 1 and sources[0].forks[0].active
    -    envelope = await bus.consume_inbound()
    -    assert envelope.message_id == raw.message_id  # type: ignore[union-attr]
    -    await bus.release_channel_inbound(envelope, InboundOwner.LANE)  # type: ignore[arg-type]
    -    assert not sources[0].forks[0].active
    -
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_ingress_cannot_claim_mobile_durable_handoff() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset({ChannelCapability.INBOUND})
    -    )
    -    sources: list[_FakeSnapshotLease] = []
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        source = _FakeSnapshotLease(snapshot)
    -        sources.append(source)
    -        return source
    -
    -    host = _host(snapshot_lease_acquirer=acquire)
    -    host.bind_inbound_publisher(MessageBus().publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    ingress = tuple(adapters.values())[0].context.ingress
    -    assert ingress is not None
    -    raw = RawInbound(
    -        message_id="forged-mobile-handoff",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="user",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={"mobile_v3_handoff": True},
    -        ),
    -    )
    -
    -    with pytest.raises(RuntimeError, match="只属于 Core akashic"):
    -        await ingress.admit(raw)
    -
    -    assert sources == []
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_external_mobile_adapter_never_receives_core_recovery_capability() -> None:
    -    module = _module(name="mobile")
    -    module.channel_names = ("mobile",)  # type: ignore[attr-defined]
    -    snapshot, factories, adapters = await _make_snapshot(
    -        module=module,
    -        capabilities=frozenset({ChannelCapability.INBOUND}),
    -    )
    -    generation = await _host().start_formal(snapshot, factories)
    -    adapter = tuple(adapters.values())[0]
    -
    -    assert not hasattr(adapter.context, "recovery_ingress")
    -    assert adapter.runtime_ports.recovery_ingress is None
    -
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_durable_recovery_replaces_retained_claim_without_weakening_duplicates(
    -    tmp_path: Any,
    -) -> None:
    -    root_token = object()
    -    adapters: dict[str, Adapter] = {}
    -
    -    def factory(context: Any) -> Adapter:
    -        adapter = Adapter(context)
    -        adapters[context.binding_token] = adapter
    -        return adapter
    -
    -    definition = CoreChannelDefinition(
    -        name="akashic",
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        ),
    -        factory=factory,
    -        inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,
    -        source_revision="core-mobile-source-1",
    -        config_revision="core-mobile-config-1",
    -        generation_id="core-mobile-generation-1",
    -    )
    -    catalog = CommittedChannelCatalog(
    -        core_definitions=(definition,),
    -        root_instance_token=root_token,
    -    )
    -    snapshot = SimpleNamespace(
    -        snapshot_id="snapshot-core-mobile",
    -        state="committed",
    -        composition_root=SimpleNamespace(instance_token=root_token),
    -        channel_catalog=catalog,
    -        channel_registry=catalog.registry,
    -        channel_registry_identity=catalog.identity,
    -        generations={},
    -    )
    -    factories = {"akashic": ClientFactory()}
    -    manager = SessionManager(tmp_path / "workspace")
    -    session_key = "akashic:retained-recovery"
    -    manager.save(manager.get_or_create(session_key))
    -    bus = MessageBus()
    -    bus.bind_durable_inbound_store(manager.control_store)
    -    bus.bind_mobile_session_admission_owner(manager)
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        return _FakeSnapshotLease(snapshot)
    -
    -    remember_calls = 0
    -
    -    async def remember(_channel: str, _identity: str, _recipient: str) -> None:
    -        nonlocal remember_calls
    -        remember_calls += 1
    -        if remember_calls == 2:
    -            raise OSError("identity store unavailable")
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        identity_resolver=lambda _channel, _identity: None,
    -        identity_rememberer=remember,
    -    )
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    adapter = tuple(adapters.values())[0]
    -    context = adapter.context
    -    assert context.ingress is not None
    -    assert not hasattr(context, "recovery_ingress")
    -    assert adapter.runtime_ports.recovery_ingress is not None
    -    bus.bind_mobile_channel_inbound_recoverer(
    -        adapter.runtime_ports.recovery_ingress.recover
    -    )
    -    raw = RawInbound(
    -        message_id="provider-retained-recovery",
    -        provider_identity="device:1",
    -        recipient="retained-recovery",
    -        message=ChannelInboundMessage(
    -            channel="akashic",
    -            sender="device:1",
    -            chat_id="retained-recovery",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={
    -                "session_key_override": session_key,
    -                "client_message_id": "provider-retained-recovery",
    -                "mobile_v3_handoff": True,
    -                "mobile_handoff_id": "handoff-retained-recovery",
    -            },
    -        ),
    -    )
    -    assert await bus.reserve_mobile_channel_handoff(raw) is True
    -    assert await context.ingress.admit(raw) is True
    -    assert await context.ingress.admit(raw) is False
    -    first = await bus.consume_inbound()
    -    assert isinstance(first, InboundEnvelope)
    -    await bus.retain_mobile_channel_inbound(first, InboundOwner.LANE)
    -
    -    with pytest.raises(OSError, match="identity store unavailable"):
    -        await bus.recover_durable_inbounds()
    -    assert await context.ingress.admit(raw) is False
    -
    -    await bus.recover_durable_inbounds()
    -    assert await context.ingress.admit(raw) is False
    -    recovered = await bus.consume_inbound()
    -    assert isinstance(recovered, InboundEnvelope)
    -    recovered.handoff(InboundOwner.LANE, InboundOwner.LOOP)
    -    await bus.complete_inbound(recovered)
    -
    -    # 进程重启后 Host 没有旧 claim,durable row 仍能由 current binding 恢复。
    -    restart_session_key = "akashic:restart-no-claim"
    -    manager.save(manager.get_or_create(restart_session_key))
    -    restart_raw = RawInbound(
    -        message_id="provider-restart-no-claim",
    -        provider_identity="device:1",
    -        recipient="restart-no-claim",
    -        message=ChannelInboundMessage(
    -                channel="akashic",
    -            sender="device:1",
    -            chat_id="restart-no-claim",
    -            content="restart",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={
    -                "session_key_override": restart_session_key,
    -                "client_message_id": "provider-restart-no-claim",
    -                "mobile_v3_handoff": True,
    -                "mobile_handoff_id": "handoff-restart-no-claim",
    -            },
    -        ),
    -    )
    -    assert await bus.reserve_mobile_channel_handoff(restart_raw) is True
    -    await bus.defer_mobile_channel_handoff("handoff-restart-no-claim")
    -    await bus.recover_durable_inbounds()
    -    assert await context.ingress.admit(restart_raw) is False
    -    restart_recovered = await bus.consume_inbound()
    -    assert isinstance(restart_recovered, InboundEnvelope)
    -    restart_recovered.handoff(InboundOwner.LANE, InboundOwner.LOOP)
    -    await bus.complete_inbound(restart_recovered)
    -
    -    assert manager.control_store.list_inbound_handoffs() == []
    -    assert remember_calls == 4
    -    await generation.stop()
    -    await bus.aclose()
    -    manager.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_only_binding_rejects_ingress_before_runtime_ports() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    acquired = 0
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        nonlocal acquired
    -        acquired += 1
    -        return _FakeSnapshotLease(snapshot)
    -
    -    published: list[InboundEnvelope] = []
    -
    -    async def publish(envelope: InboundEnvelope) -> None:
    -        published.append(envelope)
    -
    -    host = _host(snapshot_lease_acquirer=acquire)
    -    host.bind_inbound_publisher(publish)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    raw = RawInbound(
    -        message_id="provider-message-outbound-only",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="user",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    assert tuple(adapters.values())[0].context.ingress is None
    -
    -    assert acquired == 0
    -    assert published == []
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_ingress_rejects_different_stable_snapshot_and_releases_claim() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    other_snapshot = SimpleNamespace(snapshot_id="other-snapshot", generations={})
    -    wrong = _FakeSnapshotLease(other_snapshot)
    -    right = _FakeSnapshotLease(snapshot)
    -    acquired = [wrong, right]
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        return acquired.pop(0)
    -
    -    bus = MessageBus()
    -    host = _host(snapshot_lease_acquirer=acquire)
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    raw = RawInbound(
    -        message_id="provider-message-race",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="user",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -    adapter = tuple(adapters.values())[0]
    -
    -    with pytest.raises(RuntimeError, match="stable snapshot 不一致"):
    -        await adapter.context.ingress.admit(raw)
    -
    -    assert not wrong.active
    -    assert await adapter.context.ingress.admit(raw) is True
    -    envelope = await bus.consume_inbound()
    -    assert isinstance(envelope, InboundEnvelope)
    -    await bus.release_channel_inbound(envelope, InboundOwner.LANE)
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_ingress_scopes_dedupe_by_provider_identity_and_persists_mapping() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    mapping: dict[tuple[str, str], str] = {}
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        return _FakeSnapshotLease(snapshot)
    -
    -    async def remember(channel: str, identity: str, recipient: str) -> None:
    -        mapping[(channel, identity)] = recipient
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        identity_resolver=lambda channel, identity: mapping.get((channel, identity)),
    -        identity_rememberer=remember,
    -    )
    -    bus = MessageBus()
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    ingress = tuple(adapters.values())[0].context.ingress
    -    identity = tuple(adapters.values())[0].context.identity
    -    assert ingress is not None and identity is not None
    -
    -    def raw(provider: str, recipient: str) -> RawInbound:
    -        return RawInbound(
    -            message_id="same-provider-message-id",
    -            provider_identity=provider,
    -            recipient=recipient,
    -            message=ChannelInboundMessage(
    -                channel="feishu",
    -                sender=provider,
    -                chat_id=recipient,
    -                content="hello",
    -                timestamp=datetime.now(timezone.utc),
    -                metadata={},
    -            ),
    -        )
    -
    -    assert await ingress.admit(raw("open-a", "chat-a")) is True
    -    assert await ingress.admit(raw("open-b", "chat-b")) is True
    -    assert identity.resolve("open-a") == "chat-a"
    -    assert identity.resolve("open-b") == "chat-b"
    -    first = await bus.consume_inbound()
    -    second = await bus.consume_inbound()
    -    assert isinstance(first, InboundEnvelope)
    -    assert isinstance(second, InboundEnvelope)
    -    await bus.release_channel_inbound(first, InboundOwner.LANE)
    -    await bus.release_channel_inbound(second, InboundOwner.LANE)
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_identity_write_failure_releases_dedupe_claim_before_snapshot_acquire() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    acquire_calls = 0
    -    fail = True
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        nonlocal acquire_calls
    -        acquire_calls += 1
    -        return _FakeSnapshotLease(snapshot)
    -
    -    async def remember(_channel: str, _identity: str, _recipient: str) -> None:
    -        nonlocal fail
    -        if fail:
    -            fail = False
    -            raise OSError("identity store unavailable")
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        identity_resolver=lambda _channel, _identity: None,
    -        identity_rememberer=remember,
    -    )
    -    bus = MessageBus()
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    ingress = tuple(adapters.values())[0].context.ingress
    -    assert ingress is not None
    -    raw = RawInbound(
    -        message_id="identity-retry",
    -        provider_identity="open-id",
    -        recipient="chat-id",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="open-id",
    -            chat_id="chat-id",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    with pytest.raises(OSError, match="identity store unavailable"):
    -        await ingress.admit(raw)
    -
    -    assert acquire_calls == 1
    -    assert await ingress.admit(raw) is True
    -    assert acquire_calls == 2
    -    envelope = await bus.consume_inbound()
    -    assert isinstance(envelope, InboundEnvelope)
    -    await bus.release_channel_inbound(envelope, InboundOwner.LANE)
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_publisher_failure_rolls_back_identity_receipt_and_binding() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    receipt = object()
    -    rolled_back: list[object] = []
    -    sources: list[_FakeSnapshotLease] = []
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        source = _FakeSnapshotLease(snapshot)
    -        sources.append(source)
    -        return source
    -
    -    async def remember(
    -        _channel: str,
    -        _identity: str,
    -        _recipient: str,
    -    ) -> object:
    -        return receipt
    -
    -    async def rollback(value: object) -> bool:
    -        rolled_back.append(value)
    -        return True
    -
    -    async def publish(_envelope: InboundEnvelope) -> None:
    -        raise RuntimeError("publisher unavailable")
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        identity_resolver=lambda _channel, _identity: None,
    -        identity_rememberer=remember,
    -        identity_rollbacker=rollback,
    -    )
    -    host.bind_inbound_publisher(publish)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    ingress = tuple(adapters.values())[0].context.ingress
    -    assert ingress is not None
    -    raw = RawInbound(
    -        message_id="publisher-failure",
    -        provider_identity="open-id",
    -        recipient="chat-id",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="open-id",
    -            chat_id="chat-id",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    with pytest.raises(RuntimeError, match="publisher unavailable"):
    -        await ingress.admit(raw)
    -
    -    assert rolled_back == [receipt]
    -    assert generation.channel("feishu").in_flight == 0
    -    assert len(sources) == 1 and not sources[0].active
    -    assert len(sources[0].forks) == 1 and not sources[0].forks[0].active
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_identity_write_is_owned_by_binding_drain_during_publication() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(
    -        capabilities=frozenset(
    -            {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -        )
    -    )
    -    remember_started = asyncio.Event()
    -    remember_release = asyncio.Event()
    -    mapping: dict[str, str] = {}
    -
    -    def acquire(snapshot_id: str) -> _FakeSnapshotLease:
    -        assert snapshot_id == snapshot.snapshot_id
    -        return _FakeSnapshotLease(snapshot)
    -
    -    async def remember(_channel: str, identity: str, recipient: str) -> None:
    -        remember_started.set()
    -        await remember_release.wait()
    -        mapping[identity] = recipient
    -
    -    host = _host(
    -        snapshot_lease_acquirer=acquire,
    -        identity_resolver=lambda _channel, identity: mapping.get(identity),
    -        identity_rememberer=remember,
    -    )
    -    bus = MessageBus()
    -    host.bind_inbound_publisher(bus.publish_channel_inbound)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    ingress = tuple(adapters.values())[0].context.ingress
    -    assert ingress is not None
    -    raw = RawInbound(
    -        message_id="publication-race",
    -        provider_identity="open-id",
    -        recipient="chat-id",
    -        message=ChannelInboundMessage(
    -            channel="feishu",
    -            sender="open-id",
    -            chat_id="chat-id",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={},
    -        ),
    -    )
    -
    -    admission = asyncio.create_task(ingress.admit(raw))
    -    await remember_started.wait()
    -    stop = asyncio.create_task(generation.stop())
    -    await asyncio.sleep(0)
    -    assert not stop.done()
    -
    -    remember_release.set()
    -    assert await admission is True
    -    assert mapping == {"open-id": "chat-id"}
    -    envelope = await bus.consume_inbound()
    -    assert isinstance(envelope, InboundEnvelope)
    -    await bus.release_channel_inbound(envelope, InboundOwner.LANE)
    -    await stop
    -
    -
    -@pytest.mark.asyncio
    -async def test_binding_lease_cancel_waits_for_exact_snapshot_release() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    release_gate = asyncio.Event()
    -    source = _FakeSnapshotLease(snapshot, release_gate=release_gate)
    -    owner = host.acquire_binding(cast(Any, source), "feishu")
    -
    -    closing = asyncio.create_task(owner.aclose())
    -    await asyncio.sleep(0)
    -    closing.cancel()
    -    await asyncio.sleep(0)
    -    assert not closing.done()
    -    release_gate.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await closing
    -    assert not owner.active
    -    assert len(source.forks) == 1 and not source.forks[0].active
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_wrong_binding_and_receipt_identity_fail_loud() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    binding = generation.channel("feishu")
    -    binding.open_admission()
    -    with pytest.raises(RuntimeError, match="binding token"):
    -        await binding.deliver(ProviderDeliveryRequest("wrong", "d1", "u", "hi"))
    -    await generation.stop()
    -
    -    snapshot, factories, adapters = await _make_snapshot(wrong_receipt=True)
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    binding = generation.channel("feishu")
    -    binding.open_admission()
    -    for adapter in adapters.values():
    -        adapter.release.set()
    -    with pytest.raises(RuntimeError, match="receipt identity"):
    -        await binding.deliver(ProviderDeliveryRequest(binding.binding_token, "d1", "u", "hi"))
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_journal_callback_happens_before_start_and_failure_keeps_count_zero() -> None:
    -    events: list[str] = []
    -    records: list[ChannelStartRecord] = []
    -    snapshot, factories, _ = await _make_snapshot(factory_events=events)
    -
    -    async def before(record: ChannelStartRecord) -> None:
    -        records.append(record)
    -        events.append("journal")
    -
    -    async def check(record: ChannelStartRecord) -> None:
    -        events.append("config-check")
    -
    -    host = _host(on_before_start=before, config_revision_checker=check)
    -    generation = await host.start_formal(snapshot, factories)
    -    assert events == ["journal", "config-check", "factory"]
    -    assert records[0].source_revision == "source-1"
    -    assert records[0].config_revision == channel_config_revision(
    -        {"app_secret": CredentialRef(("app_secret",))}
    -    )
    -    assert records[0].raw_config_revision == "raw-config-1"
    -    assert len(records[0].descriptor_digest) == 64
    -    assert records[0].factory_export == "make_adapter"
    -    assert records[0].artifact_pointer == "/tmp/plugin"
    -    assert records[0].target == "formal"
    -    assert records[0].boot_owner == "plugin-manager"
    -    assert host.start_count(snapshot.snapshot_id, "feishu") == 1
    -    await generation.stop()
    -
    -    async def fail_before(record: ChannelStartRecord) -> None:
    -        raise RuntimeError("journal failed")
    -
    -    events = []
    -    snapshot, _, _ = await _make_snapshot(factory_events=events)
    -    host = _host(on_before_start=fail_before)
    -    with pytest.raises(RuntimeError, match="journal failed"):
    -        await host.start_formal(snapshot, {"feishu": ClientFactory()})
    -    assert host.start_count(snapshot.snapshot_id, "feishu") == 0
    -    assert events == []
    -
    -
    -def test_durable_callbacks_are_mandatory() -> None:
    -    with pytest.raises(TypeError):
    -        ChannelGenerationHost(
    -            on_before_start=None,  # type: ignore[arg-type]
    -            config_revision_checker=_noop_record,
    -        )
    -    with pytest.raises(TypeError):
    -        ChannelGenerationHost(
    -            on_before_start=_noop_record,
    -            config_revision_checker=None,  # type: ignore[arg-type]
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_identity_rollback_fence_conflict_is_fail_loud() -> None:
    -    async def remember(
    -        _channel: str,
    -        _identity: str,
    -        _recipient: str,
    -    ) -> object:
    -        return object()
    -
    -    async def rollback(_receipt: object) -> bool:
    -        return False
    -
    -    host = _host(
    -        identity_resolver=lambda _channel, _identity: None,
    -        identity_rememberer=remember,
    -        identity_rollbacker=rollback,
    -    )
    -
    -    with pytest.raises(RuntimeError, match="rollback fence 已被并发状态取代"):
    -        await host._rollback_identity_write(object())
    -
    -
    -@pytest.mark.asyncio
    -async def test_config_revision_checker_failure_is_before_factory_and_start() -> None:
    -    events: list[str] = []
    -    snapshot, factories, _ = await _make_snapshot(factory_events=events)
    -
    -    async def check(record: ChannelStartRecord) -> None:
    -        raise RuntimeError("config revision drift")
    -
    -    host = _host(config_revision_checker=check)
    -    with pytest.raises(RuntimeError, match="config revision drift"):
    -        await host.start_formal(snapshot, factories)
    -    assert events == []
    -    assert factories["feishu"].closed == 1
    -    assert host.start_count(snapshot.snapshot_id, "feishu") == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_empty_registry_is_repeatable_noop_without_lock_or_fiber_owner() -> None:
    -    snapshot, _, _ = await _make_snapshot()
    -    root_token = snapshot.composition_root.instance_token
    -    empty_channels = PluginChannels(root_token)
    -    empty_registry = _freeze_plugin_channels(empty_channels, root_token)
    -    snapshot.channel_registry = empty_registry
    -    snapshot.channel_registry_identity = empty_registry.identity
    -    snapshot.generations = {}
    -    host = _host()
    -    first = await host.start_formal(snapshot, {})
    -    second = await host.start_formal(snapshot, {})
    -    assert first.snapshot_id == second.snapshot_id == snapshot.snapshot_id
    -    assert await first.stop() == ()
    -    assert await second.stop() == ()
    -    assert host._locks == {}
    -    assert not hasattr(host, "fiber")
    -    assert not hasattr(host, "context")
    -
    -
    -@pytest.mark.asyncio
    -async def test_partial_start_rolls_back_started_adapter_and_provider_factory() -> None:
    -    module = _module()
    -    module.channel_names = ("feishu", "qqbot")  # type: ignore[attr-defined]
    -    snapshot, failing_factories, adapters = await _make_snapshot(
    -        module=module,
    -        fail_after=2,
    -    )
    -    host = _host()
    -    with pytest.raises(RuntimeError, match="start failed"):
    -        await host.start_formal(snapshot, failing_factories)
    -    assert all(factory.closed == 1 for factory in failing_factories.values())
    -    assert len(adapters) == 2
    -    assert sum(adapter.stopped for adapter in adapters.values()) == 2
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_stop_failure_retains_tombstone_and_retry_cleans_exact_owner() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(fail_stop=True)
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    with pytest.raises(RuntimeError, match="cleanup"):
    -        await generation.stop()
    -    tombstone = host.failure(snapshot.snapshot_id, "feishu")
    -    assert tombstone is not None
    -    assert tombstone.binding_token == generation.channel("feishu").binding_token
    -    assert tombstone.artifact_pointer == "/tmp/plugin"
    -    assert tombstone.factory_export == "make_adapter"
    -    assert tombstone.source_revision == "source-1"
    -    assert tombstone.config_revision == channel_config_revision(
    -        {"app_secret": CredentialRef(("app_secret",))}
    -    )
    -    assert tombstone.raw_config_revision == "raw-config-1"
    -    assert len(tombstone.descriptor_digest) == 64
    -    assert tombstone.target == "formal"
    -    assert tombstone.boot_owner == "plugin-manager"
    -    assert tombstone.adapter_stop_settled is True
    -    assert tombstone.adapter_stop_succeeded is False
    -    assert tombstone.factory_close_settled is True
    -    assert tombstone.factory_close_succeeded is True
    -    with pytest.raises(RuntimeError, match="未知"):
    -        await host.retry_generation_cleanup("wrong-binding-token")
    -    adapter = next(iter(adapters.values()))
    -    adapter.fail_stop = False
    -    await host.retry_generation_cleanup(tombstone.binding_token)
    -    assert adapter.stopped == 2
    -    assert factories["feishu"].closed == 1
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_incomplete_stop_receipt_is_diagnostic_error_and_retryable(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    snapshot, factories, adapters = await _make_snapshot(
    -        adapter_cls=IncompleteStopAdapter
    -    )
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -
    -    with pytest.raises(RuntimeError, match="cleanup"):
    -        await generation.stop()
    -
    -    tombstone = host.failure(snapshot.snapshot_id, "feishu")
    -    assert tombstone is not None
    -    assert tombstone.adapter_stop_succeeded is False
    -    stop_terminals = [
    -        _diagnostic_fields(record)
    -        for record in caplog.records
    -        if _diagnostic_fields(record).get("operation") == "channel.stop"
    -        and _diagnostic_fields(record).get("event")
    -        in {"plugin.operation.done", "plugin.operation.error"}
    -    ]
    -    assert [item["event"] for item in stop_terminals] == [
    -        "plugin.operation.error"
    -    ]
    -
    -    adapter = cast(IncompleteStopAdapter, next(iter(adapters.values())))
    -    adapter.resources_closed = True
    -    await host.retry_generation_cleanup(tombstone.binding_token)
    -    assert adapter.stopped == 2
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_retry_skips_successful_adapter_stop_when_factory_close_failed() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    factories["feishu"].fail_close = True
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    with pytest.raises(RuntimeError, match="cleanup"):
    -        await generation.stop()
    -    adapter = next(iter(adapters.values()))
    -    assert adapter.stopped == 1
    -    assert factories["feishu"].closed == 1
    -    tombstone = host.failure(snapshot.snapshot_id, "feishu")
    -    assert tombstone is not None
    -    assert tombstone.adapter_stop_succeeded is True
    -    assert tombstone.factory_close_settled is True
    -    assert tombstone.factory_close_succeeded is False
    -    factories["feishu"].fail_close = False
    -    await host.retry_generation_cleanup(tombstone.binding_token)
    -    assert adapter.stopped == 1
    -    assert factories["feishu"].closed == 2
    -
    -
    -@pytest.mark.asyncio
    -async def test_provider_cancel_and_failure_callback_cancel_retain_tombstone() -> None:
    -    snapshot, factories, _ = await _make_snapshot(cancel_stop=True)
    -
    -    async def on_failure(record: Any) -> None:
    -        raise asyncio.CancelledError
    -
    -    host = _host(on_failure=on_failure)
    -    generation = await host.start_formal(snapshot, factories)
    -    with pytest.raises(asyncio.CancelledError):
    -        await generation.stop()
    -    assert host.failure(snapshot.snapshot_id, "feishu") is not None
    -
    -
    -@pytest.mark.asyncio
    -async def test_failure_callback_error_is_not_logged_as_success() -> None:
    -    snapshot, factories, _ = await _make_snapshot(fail_stop=True)
    -
    -    async def on_failure(record: Any) -> None:
    -        raise RuntimeError("journal unavailable")
    -
    -    host = _host(on_failure=on_failure)
    -    generation = await host.start_formal(snapshot, factories)
    -    with pytest.raises(RuntimeError, match="journal unavailable"):
    -        await generation.stop()
    -    assert host.failure(snapshot.snapshot_id, "feishu") is not None
    -
    -
    -@pytest.mark.asyncio
    -async def test_factory_and_adapter_start_cancellation_keep_exact_tombstones() -> None:
    -    snapshot, factories, _ = await _make_snapshot(cancel_factory=True)
    -    host = _host()
    -    with pytest.raises(asyncio.CancelledError):
    -        await host.start_formal(snapshot, factories)
    -    factory_failure = host.failure(snapshot.snapshot_id, "feishu")
    -    assert factory_failure is not None
    -    assert factory_failure.binding_token
    -    assert factories["feishu"].closed == 1
    -    await host.retry_generation_cleanup(factory_failure.binding_token)
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -    snapshot, factories, adapters = await _make_snapshot(cancel_start=True)
    -    host = _host()
    -    with pytest.raises(asyncio.CancelledError):
    -        await host.start_formal(snapshot, factories)
    -    adapter_failure = host.failure(snapshot.snapshot_id, "feishu")
    -    assert adapter_failure is not None
    -    assert adapter_failure.adapter is next(iter(adapters.values()))
    -    assert factories["feishu"].closed == 1
    -    await host.retry_generation_cleanup(adapter_failure.binding_token)
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_async_factory_and_noncallable_factory_are_rejected_before_start() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -
    -    async def async_factory(context: Any) -> Adapter:
    -        return Adapter(context)
    -
    -    setattr(snapshot.generations["plugin.feishu"].instance.module, "make_adapter", async_factory)
    -    with pytest.raises(TypeError, match="async"):
    -        await _host().start_formal(snapshot, factories)
    -    assert factories["feishu"].closed == 1
    -
    -    snapshot, factories, _ = await _make_snapshot()
    -    setattr(snapshot.generations["plugin.feishu"].instance.module, "make_adapter", None)
    -    with pytest.raises(TypeError, match="不可调用"):
    -        await _host().start_formal(snapshot, factories)
    -    assert factories["feishu"].closed == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_exact_root_and_factory_provenance_are_required() -> None:
    -    snapshot, factories, _ = await _make_snapshot()
    -    snapshot.composition_root = SimpleNamespace(instance_token=object())
    -    with pytest.raises(RuntimeError, match="exact composition Root"):
    -        await _host().start_formal(snapshot, factories)
    -
    -    snapshot, factories, _ = await _make_snapshot()
    -    object.__setattr__(snapshot.channel_registry.factories[0], "config_revision", "drift")
    -    with pytest.raises(RuntimeError):
    -        await _host().start_formal(snapshot, factories)
    -
    -
    -@pytest.mark.asyncio
    -async def test_caller_cancellation_waits_for_cleanup() -> None:
    -    snapshot, factories, adapters = await _make_snapshot(block_stop=True)
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    stop_task = asyncio.create_task(generation.stop())
    -    adapter = next(iter(adapters.values()))
    -    await adapter.stop_started.wait()
    -    stop_task.cancel()
    -    stop_task.cancel()
    -    adapter.stop_release.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await stop_task
    -    assert factories["feishu"].closed == 1
    -    assert host.failure(snapshot.snapshot_id) is None
    -
    -
    -def test_attachment_ports_must_be_bound_as_a_pair() -> None:
    -    ref = _attachment_ref()
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(_FakeAttachmentReadLease(ref))
    -    with pytest.raises(TypeError, match="同时绑定"):
    -        _host(attachment_import=import_port)
    -    with pytest.raises(TypeError, match="同时绑定"):
    -        _host(attachment_read=read_port)
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_context_gets_per_binding_attachment_facades_and_none_without_ports() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    host = _host()
    -    generation = await host.start_formal(snapshot, factories)
    -    context = tuple(adapters.values())[0].context
    -    assert context.attachment_import is None
    -    assert context.attachment_read is None
    -    await generation.stop()
    -
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(_FakeAttachmentReadLease(ref))
    -    host = _host(attachment_import=import_port, attachment_read=read_port)
    -    generation = await host.start_formal(snapshot, factories)
    -    context = tuple(adapters.values())[0].context
    -    assert context.attachment_import is not None
    -    assert context.attachment_read is not None
    -    assert context.attachment_import is not import_port
    -    assert context.attachment_read is not read_port
    -    generation.open_admission()
    -    imported = await context.attachment_import.import_bytes(
    -        b"hello",
    -        kind=AttachmentKind.FILE,
    -        filename="report.txt",
    -        media_type="text/plain",
    -    )
    -    assert imported == ref
    -    assert import_port.calls == 1
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_held_attachment_read_lease_blocks_generation_drain() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    underlying = _FakeAttachmentReadLease(ref)
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(underlying)
    -    host = _host(attachment_import=import_port, attachment_read=read_port)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    context = tuple(adapters.values())[0].context
    -    binding = generation.channel("feishu")
    -    assert context.attachment_read is not None
    -    lease = await context.attachment_read.acquire(ref)
    -    assert binding.in_flight == 1
    -    assert await lease.read_bytes(max_bytes=5) == b"hello"
    -
    -    stopping = asyncio.create_task(generation.stop())
    -    await asyncio.sleep(0)
    -    assert not stopping.done()
    -    await lease.aclose()
    -    assert binding.in_flight == 0
    -    await stopping
    -    assert underlying.close_calls == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_attachment_import_and_acquire_failure_or_cancel_release_in_flight() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(_FakeAttachmentReadLease(ref))
    -    host = _host(attachment_import=import_port, attachment_read=read_port)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    context = tuple(adapters.values())[0].context
    -    binding = generation.channel("feishu")
    -    assert context.attachment_import is not None
    -    assert context.attachment_read is not None
    -
    -    import_port.fail = True
    -    with pytest.raises(OSError, match="import failed"):
    -        await context.attachment_import.import_bytes(
    -            b"hello",
    -            kind=AttachmentKind.FILE,
    -            filename="report.txt",
    -            media_type="text/plain",
    -        )
    -    assert binding.in_flight == 0
    -
    -    read_port.fail = True
    -    with pytest.raises(OSError, match="acquire failed"):
    -        await context.attachment_read.acquire(ref)
    -    assert binding.in_flight == 0
    -    read_port.fail = False
    -
    -    import_port.fail = False
    -    import_port.gate = asyncio.Event()
    -    import_task = asyncio.create_task(
    -        context.attachment_import.import_bytes(
    -            b"hello",
    -            kind=AttachmentKind.FILE,
    -            filename="report.txt",
    -            media_type="text/plain",
    -        )
    -    )
    -    await asyncio.sleep(0)
    -    assert binding.in_flight == 1
    -    import_task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await import_task
    -    assert binding.in_flight == 0
    -
    -    read_port.gate = asyncio.Event()
    -    acquire_task = asyncio.create_task(context.attachment_read.acquire(ref))
    -    await asyncio.sleep(0)
    -    assert binding.in_flight == 1
    -    acquire_task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await acquire_task
    -    assert binding.in_flight == 0
    -    await generation.stop()
    -
    -
    -@pytest.mark.asyncio
    -async def test_closed_or_stale_binding_rejects_attachment_before_store_call() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(_FakeAttachmentReadLease(ref))
    -    host = _host(attachment_import=import_port, attachment_read=read_port)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    context = tuple(adapters.values())[0].context
    -    assert context.attachment_import is not None
    -    generation.close_admission()
    -    with pytest.raises(RuntimeError, match="关闭"):
    -        await context.attachment_import.import_bytes(
    -            b"hello",
    -            kind=AttachmentKind.FILE,
    -            filename="report.txt",
    -            media_type="text/plain",
    -        )
    -    assert import_port.calls == 0
    -    await generation.stop()
    -    with pytest.raises(KeyError):
    -        await context.attachment_import.import_bytes(
    -            b"hello",
    -            kind=AttachmentKind.FILE,
    -            filename="report.txt",
    -            media_type="text/plain",
    -        )
    -    assert import_port.calls == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_attachment_lease_close_is_critical_under_caller_cancellation() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    close_started = asyncio.Event()
    -    close_release = asyncio.Event()
    -    underlying = _FakeAttachmentReadLease(
    -        ref,
    -        close_started=close_started,
    -        close_release=close_release,
    -    )
    -    import_port = _FakeAttachmentImportPort(ref)
    -    read_port = _FakeAttachmentReadPort(underlying)
    -    host = _host(attachment_import=import_port, attachment_read=read_port)
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    context = tuple(adapters.values())[0].context
    -    binding = generation.channel("feishu")
    -    assert context.attachment_read is not None
    -    lease = await context.attachment_read.acquire(ref)
    -    stopping = asyncio.create_task(generation.stop())
    -    await asyncio.sleep(0)
    -    assert not stopping.done()
    -
    -    closing = asyncio.create_task(lease.aclose())
    -    await close_started.wait()
    -    closing.cancel()
    -    await asyncio.sleep(0)
    -    assert not closing.done()
    -    assert binding.in_flight == 1
    -
    -    close_release.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await closing
    -    assert binding.in_flight == 0
    -    await stopping
    -    assert underlying.close_calls == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_attachment_lease_concurrent_close_releases_host_once() -> None:
    -    snapshot, factories, adapters = await _make_snapshot()
    -    ref = _attachment_ref()
    -    underlying = _FakeAttachmentReadLease(ref)
    -    host = _host(
    -        attachment_import=_FakeAttachmentImportPort(ref),
    -        attachment_read=_FakeAttachmentReadPort(underlying),
    -    )
    -    generation = await host.start_formal(snapshot, factories)
    -    generation.open_admission()
    -    context = tuple(adapters.values())[0].context
    -    binding = generation.channel("feishu")
    -    assert context.attachment_read is not None
    -    lease = await context.attachment_read.acquire(ref)
    -
    -    await asyncio.gather(lease.aclose(), lease.aclose())
    -
    -    assert binding.in_flight == 0
    -    assert underlying.close_calls == 1
    -    await generation.stop()
    diff --git a/tests/test_plugin_composition_background_job_snapshot.py b/tests/test_plugin_composition_background_job_snapshot.py
    deleted file mode 100644
    index dd513a83f..000000000
    --- a/tests/test_plugin_composition_background_job_snapshot.py
    +++ /dev/null
    @@ -1,150 +0,0 @@
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    BACKGROUND_JOBS,
    -    BackgroundJobDefinition,
    -    CompositionRoot,
    -    IntervalTrigger,
    -    PluginBackgroundJobs,
    -    PluginRuntime,
    -)
    -from agent.plugins.generation import GateResult, PluginContributions, PluginGeneration
    -from agent.plugins.generation_activity_host import ActivityHost
    -from agent.plugins.generation_job_host import BackgroundJobActivityAdapter
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.scope import PluginScope
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler, RuntimeSnapshotStore
    -from bus.event_bus import EventBus
    -
    -
    -def _generation(plugin_dir: Path) -> PluginGeneration:
    -    return PluginGeneration(
    -        plugin_id="emotion",
    -        generation_id="emotion:test",
    -        module_path="plugins.emotion",
    -        source_revision="source",
    -        config_revision="config",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        config=None,
    -        instance=object(),
    -        scope=PluginScope("emotion"),
    -        contributions=PluginContributions(manifest={}),
    -        gate_result=GateResult(
    -            gate_id="gate",
    -            plugin_id="emotion",
    -            candidate_revision="source",
    -            status="passed",
    -            checks=(),
    -        ),
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_freezes_exact_background_job_catalog(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = tmp_path / "emotion"
    -    plugin_dir.mkdir()
    -    root = CompositionRoot("emotion:test")
    -    jobs = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, jobs)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(BACKGROUND_JOBS).register(
    -            ctx,
    -            BackgroundJobDefinition(
    -                name="merge_pending",
    -                triggers=(IntervalTrigger(60),),
    -                handler_export="runtime.merge_pending",
    -            ),
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="emotion",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=PluginRuntime(
    -            plugin_id="emotion",
    -            generation_id="test-generation",
    -            plugin_dir=plugin_dir,
    -            data_dir=plugin_dir / "data",
    -            workspace=plugin_dir / "workspace",
    -            config=None,
    -        ),
    -    )
    -    generation = _generation(plugin_dir)
    -    snapshot = RuntimeSnapshotCompiler().compile(
    -        {generation.plugin_id: generation},
    -        composition_root=root,
    -    )
    -
    -    catalog = snapshot.background_job_catalog
    -    assert catalog is not None
    -    binding = catalog["emotion:merge_pending"]
    -    assert binding is not None
    -    assert binding.generation_id == generation.generation_id
    -    assert snapshot.background_job_catalog_identity == catalog.identity
    -    assert catalog.root_instance_token is root.instance_token
    -    store = RuntimeSnapshotStore()
    -    store.install(snapshot)
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_provides_and_compiles_background_job_service(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = tmp_path / "plugins" / "emotion"
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import (\n"
    -        "    BACKGROUND_JOBS, BackgroundJobDefinition, IntervalTrigger,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'emotion'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (BACKGROUND_JOBS,)\n"
    -        "async def merge_pending(ctx):\n"
    -        "    return None\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(BACKGROUND_JOBS).register(ctx, BackgroundJobDefinition(\n"
    -        "        name='merge_pending',\n"
    -        "        triggers=(IntervalTrigger(60),),\n"
    -        "        handler_export='merge_pending',\n"
    -        "    ))\n",
    -        encoding="utf-8",
    -    )
    -    event_bus = EventBus()
    -    workspace = tmp_path / "workspace"
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=event_bus,
    -        tool_registry=None,
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_activity_host(
    -        ActivityHost(
    -            (
    -                BackgroundJobActivityAdapter(
    -                    manager.snapshot_store,
    -                    workspace=str(workspace),
    -                ),
    -            )
    -        )
    -    )
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    catalog = snapshot.background_job_catalog
    -    assert catalog is not None
    -    binding = catalog["emotion:merge_pending"]
    -    generation = manager.generation("emotion")
    -    assert binding is not None and generation is not None
    -    assert binding.generation_id == generation.generation_id
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_background_jobs.py b/tests/test_plugin_composition_background_jobs.py
    deleted file mode 100644
    index 7f76edcfa..000000000
    --- a/tests/test_plugin_composition_background_jobs.py
    +++ /dev/null
    @@ -1,302 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -from typing import cast
    -
    -import pytest
    -
    -from agent.plugin_composition import CompositionRoot, PluginRuntime
    -from agent.plugin_composition.background_jobs import (
    -    BACKGROUND_JOBS,
    -    BackgroundJobDefinition,
    -    IntervalTrigger,
    -    PluginBackgroundJobs,
    -    RetryPolicy,
    -    _freeze_plugin_background_jobs,
    -)
    -from agent.plugin_composition.model import CompositionError
    -
    -
    -def _runtime(tmp_path: Path, plugin_id: str = "drift") -> PluginRuntime:
    -    plugin_dir = tmp_path / plugin_id
    -    plugin_dir.mkdir(parents=True)
    -    return PluginRuntime(
    -        plugin_id=plugin_id,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=tmp_path / "data" / plugin_id,
    -        workspace=tmp_path / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _definition(
    -    name: str = "merge_proactive_pending",
    -    *,
    -    programmatic_turns: bool = False,
    -) -> BackgroundJobDefinition:
    -    return BackgroundJobDefinition(
    -        name=name,
    -        triggers=(IntervalTrigger(60),),
    -        handler_export="merge_pending",
    -        debounce_seconds=5,
    -        coalesce=True,
    -        retry_policy=RetryPolicy(
    -            max_attempts=2,
    -            base_delay_seconds=1.0,
    -            max_delay_seconds=10.0,
    -        ),
    -        model_role="agent",
    -        programmatic_turns=programmatic_turns,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_registry_freezes_binding_and_live_fence(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("jobs-generation-1")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(BACKGROUND_JOBS).register(ctx, _definition())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path),
    -    )
    -    catalog = _freeze_plugin_background_jobs(service, root.instance_token)
    -    binding = catalog["drift:merge_proactive_pending"]
    -    assert binding is not None
    -    assert binding.generation_id == "jobs-generation-1"
    -    assert binding.plugin_id == "drift"
    -    assert binding.handler_export == "merge_pending"
    -    assert binding.is_live()
    -    assert catalog["drift:merge_proactive_pending"] is binding
    -    assert _freeze_plugin_background_jobs(service, root.instance_token) is catalog
    -
    -    await fiber.dispose()
    -    assert not binding.is_live()
    -    assert _freeze_plugin_background_jobs(service, root.instance_token) is catalog
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_identity_ignores_generation_root_and_runtime_paths(
    -    tmp_path: Path,
    -) -> None:
    -    identities: list[str] = []
    -    for suffix in ("candidate", "formal"):
    -        root = CompositionRoot(f"jobs-{suffix}")
    -        service = PluginBackgroundJobs(root.instance_token)
    -        _ = await root.context.provide(BACKGROUND_JOBS, service)
    -
    -        async def apply(ctx) -> None:
    -            await ctx.require(BACKGROUND_JOBS).register(ctx, _definition())
    -
    -        _ = await root.mount(
    -            apply,
    -            name="drift",
    -            inject=(BACKGROUND_JOBS,),
    -            runtime=_runtime(tmp_path / suffix),
    -        )
    -        identities.append(
    -            _freeze_plugin_background_jobs(service, root.instance_token).identity
    -        )
    -        await root.dispose()
    -    assert identities[0] == identities[1]
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_candidate_freeze_has_no_execution_surface(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("jobs-candidate")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -    invocation_count = 0
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(BACKGROUND_JOBS).register(ctx, _definition())
    -
    -    _ = await root.mount(
    -        apply,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path),
    -    )
    -    catalog = _freeze_plugin_background_jobs(service, root.instance_token)
    -    assert invocation_count == 0
    -    assert len(catalog.descriptors) == 1
    -    assert catalog.descriptors[0].triggers == (IntervalTrigger(60),)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_preserves_explicit_programmatic_turn_declaration(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("jobs-programmatic")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(BACKGROUND_JOBS).register(
    -            ctx,
    -            _definition(programmatic_turns=True),
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path),
    -    )
    -    catalog = _freeze_plugin_background_jobs(service, root.instance_token)
    -    binding = catalog["drift:merge_proactive_pending"]
    -    assert binding is not None
    -    assert binding.definition.programmatic_turns is True
    -    assert catalog.descriptors[0].programmatic_turns is True
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_rejects_duplicate_after_freeze_and_cleans_on_failure(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("jobs-freeze")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -    captured = None
    -
    -    async def apply(ctx) -> None:
    -        nonlocal captured
    -        captured = ctx
    -        facade = ctx.require(BACKGROUND_JOBS)
    -        await facade.register(ctx, _definition())
    -        await facade.register(ctx, _definition())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path),
    -    )
    -    assert fiber.state.value == "failed"
    -    assert len(_freeze_plugin_background_jobs(service, root.instance_token)) == 0
    -    assert captured is not None
    -    await root.dispose()
    -
    -    root = CompositionRoot("jobs-freeze-after")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -    captured_after = None
    -
    -    async def apply_once(ctx) -> None:
    -        nonlocal captured_after
    -        captured_after = ctx
    -        await ctx.require(BACKGROUND_JOBS).register(ctx, _definition())
    -
    -    _ = await root.mount(
    -        apply_once,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path / "after"),
    -    )
    -    _ = _freeze_plugin_background_jobs(service, root.instance_token)
    -    assert captured_after is not None
    -    with pytest.raises(CompositionError, match="已冻结"):
    -        await service.register(captured_after, _definition())
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_rejects_cross_root_registration(
    -    tmp_path: Path,
    -) -> None:
    -    root_a = CompositionRoot("jobs-a")
    -    root_b = CompositionRoot("jobs-b")
    -    service_a = PluginBackgroundJobs(root_a.instance_token)
    -    service_b = PluginBackgroundJobs(root_b.instance_token)
    -    _ = await root_a.context.provide(BACKGROUND_JOBS, service_a)
    -    _ = await root_b.context.provide(BACKGROUND_JOBS, service_b)
    -
    -    async def apply_wrong_root(ctx) -> None:
    -        await service_a.register(ctx, _definition())
    -
    -    _ = await root_b.mount(
    -        apply_wrong_root,
    -        name="drift",
    -        inject=(BACKGROUND_JOBS,),
    -        runtime=_runtime(tmp_path),
    -    )
    -    assert any(
    -        "Service 不属于当前 Root" in (fiber.error or "")
    -        for fiber in root_b.receipt().fibers
    -    )
    -    assert len(_freeze_plugin_background_jobs(service_a, root_a.instance_token)) == 0
    -    assert len(_freeze_plugin_background_jobs(service_b, root_b.instance_token)) == 0
    -    await root_a.dispose()
    -    await root_b.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_job_name_is_unique_per_owner(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("jobs-multi-owner")
    -    service = PluginBackgroundJobs(root.instance_token)
    -    _ = await root.context.provide(BACKGROUND_JOBS, service)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(BACKGROUND_JOBS).register(ctx, _definition("refresh"))
    -
    -    for plugin_id in ("emotion", "fitbit"):
    -        _ = await root.mount(
    -            apply,
    -            name=plugin_id,
    -            inject=(BACKGROUND_JOBS,),
    -            runtime=_runtime(tmp_path, plugin_id),
    -        )
    -
    -    catalog = _freeze_plugin_background_jobs(service, root.instance_token)
    -    assert tuple(catalog) == ("emotion:refresh", "fitbit:refresh")
    -    assert catalog.get("refresh") is None
    -    await root.dispose()
    -
    -
    -@pytest.mark.parametrize(
    -    "factory",
    -    (
    -        lambda: IntervalTrigger(0),
    -        lambda: IntervalTrigger(True),
    -        lambda: BackgroundJobDefinition("bad", (), "run"),
    -        lambda: BackgroundJobDefinition(
    -            "bad",
    -            (IntervalTrigger(1),) * 2,
    -            "run",
    -        ),
    -        lambda: BackgroundJobDefinition("bad", (IntervalTrigger(1),), "bad export"),
    -        lambda: BackgroundJobDefinition(
    -            "bad",
    -            (IntervalTrigger(1),),
    -            "run",
    -            programmatic_turns=cast(bool, 1),
    -        ),
    -        lambda: RetryPolicy(max_attempts=0),
    -        lambda: RetryPolicy(base_delay_seconds=float("nan")),
    -        lambda: RetryPolicy(max_delay_seconds=float("inf")),
    -        lambda: BackgroundJobDefinition(
    -            "bad_role",
    -            (IntervalTrigger(1),),
    -            "run",
    -            model_role="proactive.merge",
    -        ),
    -    ),
    -)
    -def test_background_job_models_reject_invalid_contract(factory) -> None:
    -    with pytest.raises((TypeError, ValueError)):
    -        factory()
    diff --git a/tests/test_plugin_composition_channels.py b/tests/test_plugin_composition_channels.py
    deleted file mode 100644
    index c82c81eee..000000000
    --- a/tests/test_plugin_composition_channels.py
    +++ /dev/null
    @@ -1,807 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import json
    -from datetime import datetime, timezone
    -from pathlib import Path
    -from types import SimpleNamespace
    -from unittest.mock import AsyncMock
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    CHANNELS,
    -    AttachmentKind,
    -    AttachmentReadLease,
    -    AttachmentRef,
    -    ChannelAttachmentImportPort,
    -    ChannelAttachmentReadPort,
    -    ChannelCapability,
    -    ChannelCommitRole,
    -    ChannelDeliveryReceipt,
    -    ChannelInboundMessage,
    -    ChannelCleanupFailure,
    -    ChannelDefinition,
    -    ChannelFactoryContext,
    -    ChannelReady,
    -    ChannelTerminalStatus,
    -    CompositionError,
    -    CompositionRoot,
    -    CredentialRef,
    -    DeliveryStatus,
    -    InboundIdentity,
    -    InboundEnvelope,
    -    InboundOwner,
    -    InboundState,
    -    OutboundEnvelope,
    -    PluginChannels,
    -    PluginRuntime,
    -    ProviderDeliveryReceipt,
    -    ProviderDeliveryRequest,
    -    PushToolRequest,
    -    QueuedReceipt,
    -    RawInbound,
    -    StopReceipt,
    -)
    -from agent.plugin_composition.channels import (
    -    ChannelDescriptor,
    -    ChannelFactoryFreezeInput,
    -    ChannelFactoryProvenance,
    -    ChannelRegistrySnapshot,
    -    _freeze_plugin_channels,
    -    _registry_identity,
    -    channel_config_revision,
    -)
    -from agent.plugins.channel_generation_host import ChannelBindingLease
    -
    -
    -def _runtime(plugin_id: str, root: Path, *, generation: str = "plugin-generation") -> PluginRuntime:
    -    plugin_dir = root / plugin_id
    -    plugin_dir.mkdir(parents=True, exist_ok=True)
    -    return PluginRuntime(
    -        plugin_id=plugin_id,
    -        generation_id=generation,
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _definition(name: str = "feishu") -> ChannelDefinition:
    -    return ChannelDefinition(
    -        name=name,
    -        capabilities=frozenset(ChannelCapability),
    -        factory_export=f"{name}:build_channel",
    -        inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,
    -        credential_paths=("app_id", "app_secret"),
    -    )
    -
    -
    -def _provenance(name: str, *, generation: str = "plugin-generation") -> ChannelFactoryProvenance:
    -    definition = _definition(name)
    -    return ChannelFactoryProvenance(
    -        plugin_id="plugin",
    -        generation_id=generation,
    -        channel_name=name,
    -        source_revision="source-1",
    -        config_revision="config-1",
    -        factory_export=definition.factory_export,
    -    )
    -
    -
    -def _attachment(
    -    *,
    -    artifact_id: str = "artifact-1",
    -    kind: AttachmentKind = AttachmentKind.FILE,
    -    filename: str | None = "report.txt",
    -    media_type: str | None = "text/plain",
    -    size_bytes: int = 3,
    -    sha256: str = "a" * 64,
    -) -> AttachmentRef:
    -    return AttachmentRef(
    -        artifact_id=artifact_id,
    -        kind=kind,
    -        filename=filename,
    -        media_type=media_type,
    -        size_bytes=size_bytes,
    -        sha256=sha256,
    -    )
    -
    -
    -class _Lease(ChannelBindingLease):
    -    snapshot_lease = object()
    -
    -    def __init__(self) -> None:
    -        self.close_calls = 0
    -        self.started = asyncio.Event()
    -        self.release = asyncio.Event()
    -
    -    @property
    -    def snapshot_id(self) -> str:
    -        return "snapshot"
    -
    -    @property
    -    def generation_id(self) -> str:
    -        return "generation"
    -
    -    @property
    -    def channel_name(self) -> str:
    -        return "feishu"
    -
    -    @property
    -    def binding_token(self) -> str:
    -        return "binding"
    -
    -    @property
    -    def active(self) -> bool:
    -        return self.close_calls == 0
    -
    -    async def aclose(self) -> None:
    -        self.close_calls += 1
    -        self.started.set()
    -        await self.release.wait()
    -
    -
    -class _Ingress:
    -    async def admit(self, raw: RawInbound) -> bool:
    -        return True
    -
    -
    -def _inbound_envelope(lease: _Lease | None = None) -> InboundEnvelope:
    -    actual_lease = lease or _Lease()
    -    message = ChannelInboundMessage(
    -        channel="feishu",
    -        sender="sender",
    -        chat_id="chat",
    -        content="hello",
    -        timestamp=datetime.now(timezone.utc),
    -        metadata=json.loads('{"nested": {"items": [1, "two"]}}'),
    -    )
    -    return InboundEnvelope(
    -        message_id="provider-message",
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        message=message,
    -        lease=actual_lease,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_registry_registration_health_freeze_and_effect_cleanup(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("root-generation")
    -    channels = PluginChannels(root.instance_token)
    -    await root.context.provide(CHANNELS, channels)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(CHANNELS).register(ctx, _definition())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="plugin",
    -        runtime=_runtime("plugin", tmp_path),
    -        inject=(CHANNELS,),
    -    )
    -    snapshot = _freeze_plugin_channels(
    -        channels,
    -        root.instance_token,
    -        factory_provenance_by_owner={
    -            "plugin": ChannelFactoryFreezeInput(
    -                "plugin-generation",
    -                source_revision="source-1",
    -                config_revision="config-1",
    -            )
    -        },
    -    )
    -    assert snapshot.descriptors[0].owner == "plugin"
    -    assert snapshot.descriptors[0].capabilities == tuple(
    -        sorted(ChannelCapability, key=lambda item: item.value)
    -    )
    -    assert snapshot.factories[0].source_revision == "source-1"
    -    assert root.receipt().health[0].required is True
    -    assert root.receipt().effects == (
    -        "root:service:core.channels",
    -        "plugin:channel:feishu",
    -        "plugin:health:channel:feishu",
    -    )
    -
    -    await fiber.dispose()
    -    assert _freeze_plugin_channels(channels, root.instance_token) is snapshot
    -    assert root.receipt().effects == ("root:service:core.channels",)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_registry_rejects_duplicate_frozen_and_wrong_root(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("root-generation")
    -    channels = PluginChannels(root.instance_token)
    -    await root.context.provide(CHANNELS, channels)
    -    captured = None
    -
    -    async def duplicate(ctx) -> None:
    -        nonlocal captured
    -        captured = ctx
    -        service = ctx.require(CHANNELS)
    -        await service.register(ctx, _definition())
    -        await service.register(ctx, _definition())
    -
    -    _ = await root.mount(
    -        duplicate,
    -        name="plugin",
    -        runtime=_runtime("plugin", tmp_path),
    -        inject=(CHANNELS,),
    -    )
    -    assert not root.receipt().ready
    -    assert root.receipt().health == ()
    -    assert len(_freeze_plugin_channels(channels, root.instance_token).descriptors) == 0
    -    assert captured is not None
    -
    -    other = CompositionRoot("other-generation")
    -    other_channels = PluginChannels(other.instance_token)
    -    await other.context.provide(CHANNELS, other_channels)
    -    with pytest.raises(CompositionError, match="不属于当前 Root"):
    -        await channels.register(other.context, _definition())
    -
    -    await other.dispose()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_channel_registry_identity_is_root_independent_and_ordered(
    -    tmp_path: Path,
    -) -> None:
    -    identities: list[str] = []
    -    for suffix, names in (("candidate", ("qqbot", "feishu")), ("formal", ("feishu", "qqbot"))):
    -        root = CompositionRoot(f"{suffix}-root")
    -        channels = PluginChannels(root.instance_token)
    -        await root.context.provide(CHANNELS, channels)
    -
    -        async def apply(ctx) -> None:
    -            service = ctx.require(CHANNELS)
    -            for name in names:
    -                await service.register(ctx, _definition(name))
    -
    -        _ = await root.mount(
    -            apply,
    -            name="plugin",
    -            runtime=_runtime("plugin", tmp_path / suffix),
    -            inject=(CHANNELS,),
    -        )
    -        snapshot = _freeze_plugin_channels(
    -            channels,
    -            root.instance_token,
    -            factory_provenance_by_owner={
    -                "plugin": ChannelFactoryFreezeInput(
    -                    generation_id="same-generation",
    -                    source_revision="same-source",
    -                    config_revision="same-config",
    -                )
    -            },
    -        )
    -        identities.append(snapshot.identity)
    -        assert tuple(item.channel_name for item in snapshot.factories) == (
    -            "feishu",
    -            "qqbot",
    -        )
    -        assert all(item.plugin_id == "plugin" for item in snapshot.factories)
    -        assert snapshot.root_instance_token is root.instance_token
    -        await root.dispose()
    -    assert identities[0] == identities[1]
    -
    -
    -def test_channel_config_revision_uses_redacted_projection() -> None:
    -    first = {
    -        "app_id": "app-1",
    -        "app_secret": CredentialRef(("app_secret",)),
    -        "options": {"retry": 2, "delay": 0.25},
    -    }
    -    reordered = {
    -        "options": {"delay": 0.25, "retry": 2},
    -        "app_secret": CredentialRef(("app_secret",)),
    -        "app_id": "app-1",
    -    }
    -    changed = {**first, "app_id": "app-2"}
    -
    -    assert channel_config_revision(first) == channel_config_revision(reordered)
    -    assert channel_config_revision(first) != channel_config_revision(changed)
    -
    -
    -def test_channel_declarations_and_provenance_reject_invalid_values() -> None:
    -    with pytest.raises(ValueError):
    -        _ = _definition("BadName")
    -    with pytest.raises(ValueError):
    -        _ = ChannelDefinition(
    -            name="feishu",
    -            capabilities=frozenset({"inbound"}),  # type: ignore[arg-type]
    -            factory_export=lambda: None,  # type: ignore[arg-type]
    -            inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,
    -            credential_paths=("app_id", "app_id"),
    -        )
    -    with pytest.raises(ValueError):
    -        _ = CredentialRef(("app_secret", ".."))
    -
    -
    -def test_channel_inbound_identity_matches_declared_capability() -> None:
    -    outbound = ChannelDefinition(
    -        name="push",
    -        capabilities=frozenset({ChannelCapability.OUTBOUND}),
    -        factory_export="push:build_channel",
    -        inbound_identity=None,
    -        credential_paths=("token",),
    -    )
    -    assert outbound.inbound_identity is None
    -
    -    with pytest.raises(ValueError, match="必须声明 inbound_identity"):
    -        _ = ChannelDefinition(
    -            name="inbound",
    -            capabilities=frozenset({ChannelCapability.INBOUND}),
    -            factory_export="inbound:build_channel",
    -            inbound_identity=None,
    -            credential_paths=("token",),
    -        )
    -    with pytest.raises(ValueError, match="不得声明 inbound_identity"):
    -        _ = ChannelDefinition(
    -            name="push",
    -            capabilities=frozenset({ChannelCapability.OUTBOUND}),
    -            factory_export="push:build_channel",
    -            inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,
    -            credential_paths=("token",),
    -        )
    -
    -
    -def test_channel_snapshot_identity_is_content_addressed() -> None:
    -    descriptor = _definition()
    -    frozen_descriptor = ChannelDescriptor(
    -        owner="plugin",
    -        name=descriptor.name,
    -        capabilities=tuple(sorted(descriptor.capabilities, key=lambda item: item.value)),
    -        factory_export=descriptor.factory_export,
    -        inbound_identity=descriptor.inbound_identity,
    -        credential_paths=descriptor.credential_paths,
    -    )
    -    provenance = _provenance("feishu")
    -    snapshot = ChannelRegistrySnapshot(
    -        descriptors=(frozen_descriptor,),
    -        factories=(provenance,),
    -        identity=_registry_identity((frozen_descriptor,), (provenance,)),
    -        root_instance_token=object(),
    -    )
    -    assert snapshot.identity
    -    with pytest.raises(ValueError, match="identity"):
    -        _ = ChannelRegistrySnapshot(
    -            descriptors=snapshot.descriptors,
    -            factories=snapshot.factories,
    -            identity="not-the-digest",
    -            root_instance_token=object(),
    -        )
    -
    -    with pytest.raises(ValueError, match="名称重复"):
    -        _ = ChannelRegistrySnapshot(
    -            descriptors=(frozen_descriptor, frozen_descriptor),
    -            factories=(provenance, provenance),
    -            identity="unused",
    -            root_instance_token=object(),
    -        )
    -
    -
    -def test_channel_factory_context_freezes_config_and_credential_refs() -> None:
    -    class ProviderFactory:
    -        async def create(self, credentials):  # type: ignore[no-untyped-def]
    -            raise AssertionError(credentials)
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    raw = {"options": {"retry": [1, 2]}, "token": CredentialRef(("token",))}
    -    context = ChannelFactoryContext(
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        config=raw,
    -        credentials={"token": CredentialRef(("token",))},
    -        provider_client_factory=ProviderFactory(),
    -        ingress=_Ingress(),
    -        identity=None,
    -    )
    -
    -    raw["options"] = {"retry": [99]}
    -    assert context.config["options"]["retry"] == (1, 2)  # type: ignore[index]
    -    assert context.credentials["token"] == CredentialRef(("token",))
    -    with pytest.raises(TypeError):
    -        context.config["new"] = "value"  # type: ignore[index]
    -    with pytest.raises(ValueError, match="path 与 ref"):
    -        _ = ChannelFactoryContext(
    -            snapshot_id="snapshot",
    -            generation_id="generation",
    -            binding_token="binding",
    -            config={},
    -            credentials={"token": CredentialRef(("other",))},
    -            provider_client_factory=ProviderFactory(),
    -            ingress=_Ingress(),
    -            identity=None,
    -        )
    -
    -
    -def test_channel_provider_delivery_and_cleanup_receipts_are_typed() -> None:
    -    request = ProviderDeliveryRequest(
    -        binding_token="binding",
    -        delivery_id="delivery",
    -        recipient="recipient",
    -        body="",
    -        commit_role=ChannelCommitRole.PASSIVE,
    -        thinking="thinking",
    -        reply_to="reply",
    -        session_message_id="message",
    -        control_turn_id="turn",
    -        execution_attempt_id="attempt",
    -        terminal_status=ChannelTerminalStatus.COMPLETED,
    -    )
    -    receipt = ProviderDeliveryReceipt(
    -        delivery_id=request.delivery_id,
    -        status=DeliveryStatus.DELIVERED,
    -        provider_ids=("remote-1",),
    -    )
    -    failure = ChannelCleanupFailure(
    -        stage="stop",
    -        plugin_id="plugin",
    -        generation_id="generation",
    -        binding_token=request.binding_token,
    -        resource="adapter",
    -        error_type="RuntimeError",
    -        message="stop failed",
    -        retry_action="retry_generation_cleanup",
    -    )
    -
    -    assert ChannelReady(request.binding_token).admission_open is False
    -    assert request.commit_role is ChannelCommitRole.PASSIVE
    -    assert request.terminal_status is ChannelTerminalStatus.COMPLETED
    -    assert receipt.status is DeliveryStatus.DELIVERED
    -    assert StopReceipt(
    -        request.binding_token,
    -        resources_closed=False,
    -        failures=(failure,),
    -    ).failures == (failure,)
    -
    -
    -def test_attachment_ref_and_channel_payloads_are_frozen_and_typed() -> None:
    -    attachment = _attachment(
    -        kind=AttachmentKind.IMAGE,
    -        filename=None,
    -        media_type="image/png",
    -    )
    -    message = ChannelInboundMessage(
    -        channel="feishu",
    -        sender="sender",
    -        chat_id="chat",
    -        content="hello",
    -        timestamp=datetime.now(timezone.utc),
    -        metadata={},
    -        attachments=(attachment,),
    -    )
    -    outbound = OutboundEnvelope(
    -        logical_delivery_id="delivery",
    -        delivery_id="delivery",
    -        attempt_sequence=1,
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        channel="feishu",
    -        recipient="chat",
    -        body="hello",
    -        metadata={},
    -        attachments=(attachment,),
    -    )
    -    request = ProviderDeliveryRequest(
    -        binding_token="binding",
    -        delivery_id="delivery",
    -        recipient="chat",
    -        body="hello",
    -        attachments=(attachment,),
    -    )
    -    push = PushToolRequest(
    -        channel="feishu",
    -        recipient="chat",
    -        body="hello",
    -        metadata={},
    -        attachments=(attachment,),
    -    )
    -
    -    assert message.attachments == (attachment,)
    -    assert outbound.attachments == (attachment,)
    -    assert request.attachments == (attachment,)
    -    assert push.attachments == (attachment,)
    -    with pytest.raises((AttributeError, TypeError)):
    -        attachment.artifact_id = "changed"  # type: ignore[misc]
    -    with pytest.raises(TypeError, match="attachments 必须是 tuple"):
    -        _ = PushToolRequest(
    -            channel="feishu",
    -            recipient="chat",
    -            body="hello",
    -            metadata={},
    -            attachments=[attachment],  # type: ignore[arg-type]
    -        )
    -    with pytest.raises(TypeError, match="AttachmentRef"):
    -        _ = ProviderDeliveryRequest(
    -            binding_token="binding",
    -            delivery_id="delivery",
    -            recipient="chat",
    -            body="hello",
    -            attachments=("not-a-ref",),  # type: ignore[arg-type]
    -        )
    -
    -
    -def test_attachment_ref_rejects_unsafe_identity_metadata_and_digest() -> None:
    -    cases = (
    -        {"artifact_id": "../escape"},
    -        {"artifact_id": "/absolute"},
    -        {"kind": "file"},
    -        {"filename": "../report.txt"},
    -        {"filename": ""},
    -        {"media_type": "text"},
    -        {"size_bytes": -1},
    -        {"size_bytes": True},
    -        {"sha256": "A" * 64},
    -        {"sha256": "a" * 63},
    -    )
    -    for overrides in cases:
    -        values = {
    -            "artifact_id": "artifact-1",
    -            "kind": AttachmentKind.FILE,
    -            "filename": "report.txt",
    -            "media_type": "text/plain",
    -            "size_bytes": 3,
    -            "sha256": "a" * 64,
    -        }
    -        values.update(overrides)
    -        with pytest.raises((TypeError, ValueError)):
    -            _ = AttachmentRef(**values)  # type: ignore[arg-type]
    -
    -
    -def test_attachment_ports_are_exported_and_factory_context_validates_them() -> None:
    -    class ImportPort:
    -        async def import_bytes(
    -            self,
    -            data,
    -            *,
    -            kind,
    -            filename,
    -            media_type,
    -        ):  # type: ignore[no-untyped-def]
    -            raise AssertionError((data, kind, filename, media_type))
    -
    -    class ReadPort:
    -        async def acquire(self, ref):  # type: ignore[no-untyped-def]
    -            raise AssertionError(ref)
    -
    -    class ProviderFactory:
    -        async def create(self, credentials):  # type: ignore[no-untyped-def]
    -            raise AssertionError(credentials)
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    context = ChannelFactoryContext(
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        config={},
    -        credentials={},
    -        provider_client_factory=ProviderFactory(),
    -        ingress=None,
    -        identity=None,
    -        attachment_import=ImportPort(),
    -        attachment_read=ReadPort(),
    -    )
    -    assert context.attachment_import is not None
    -    assert callable(context.attachment_import.import_bytes)
    -    assert context.attachment_read is not None
    -    assert callable(context.attachment_read.acquire)
    -    assert ChannelAttachmentImportPort
    -    assert ChannelAttachmentReadPort
    -    assert AttachmentReadLease
    -
    -    with pytest.raises(TypeError, match="attachment_import"):
    -        _ = ChannelFactoryContext(
    -            snapshot_id="snapshot",
    -            generation_id="generation",
    -            binding_token="binding",
    -            config={},
    -            credentials={},
    -            provider_client_factory=ProviderFactory(),
    -            ingress=None,
    -            identity=None,
    -            attachment_import=object(),  # type: ignore[arg-type]
    -        )
    -
    -
    -def test_c14c_metadata_is_recursively_frozen_and_rejects_unsafe_values() -> None:
    -    metadata = json.loads('{"nested": {"items": [1, "two"]}}')
    -    message = ChannelInboundMessage(
    -        channel="feishu",
    -        sender="sender",
    -        chat_id="chat",
    -        content="hello",
    -        timestamp=datetime.now(timezone.utc),
    -        metadata=metadata,
    -    )
    -    outbound = OutboundEnvelope(
    -        logical_delivery_id="delivery",
    -        delivery_id="delivery",
    -        attempt_sequence=1,
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        channel="feishu",
    -        recipient="chat",
    -        body="hello",
    -        metadata=metadata,
    -    )
    -    push = PushToolRequest(
    -        channel="feishu",
    -        recipient="chat",
    -        body="hello",
    -        metadata=metadata,
    -    )
    -    metadata["nested"]["items"].append("source mutation")
    -    assert message.metadata["nested"]["items"] == (1, "two")  # type: ignore[index]
    -    assert outbound.metadata["nested"]["items"] == (1, "two")  # type: ignore[index]
    -    assert push.metadata["nested"]["items"] == (1, "two")  # type: ignore[index]
    -
    -    with pytest.raises(TypeError):
    -        message.metadata["new"] = "value"  # type: ignore[index]
    -    with pytest.raises(ValueError, match="非有限"):
    -        _ = ChannelInboundMessage(
    -            channel="feishu",
    -            sender="sender",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(timezone.utc),
    -            metadata={"bad": float("nan")},
    -        )
    -    with pytest.raises(ValueError, match="timezone-aware"):
    -        _ = ChannelInboundMessage(
    -            channel="feishu",
    -            sender="sender",
    -            chat_id="chat",
    -            content="hello",
    -            timestamp=datetime.now(),
    -            metadata={},
    -        )
    -    with pytest.raises(TypeError, match="值类型无效"):
    -        _ = PushToolRequest(
    -            channel="feishu",
    -            recipient="chat",
    -            body="hello",
    -            metadata={"bad": {"not", "json"}},  # type: ignore[dict-item]
    -        )
    -
    -
    -def test_channel_text_accepts_layout_controls_and_rejects_nul() -> None:
    -    body = "line one\nline two\tvalue\r\n"
    -    message = ChannelInboundMessage(
    -        channel="feishu",
    -        sender="sender",
    -        chat_id="chat",
    -        content=body,
    -        timestamp=datetime.now(timezone.utc),
    -        metadata={},
    -    )
    -    outbound = OutboundEnvelope(
    -        logical_delivery_id="delivery",
    -        delivery_id="delivery",
    -        attempt_sequence=1,
    -        snapshot_id="snapshot",
    -        generation_id="generation",
    -        binding_token="binding",
    -        channel="feishu",
    -        recipient="chat",
    -        body=body,
    -        metadata={},
    -    )
    -
    -    assert message.content == body
    -    assert outbound.body == body
    -    with pytest.raises(ValueError, match="控制字符"):
    -        _ = PushToolRequest(
    -            channel="feishu",
    -            recipient="chat",
    -            body="unsafe\x00body",
    -            metadata={},
    -        )
    -
    -
    -def test_raw_inbound_and_outbound_receipts_enforce_identity_contract() -> None:
    -    envelope = _inbound_envelope()
    -    raw = RawInbound(message_id="provider-message", message=envelope.message)
    -    assert raw.message is envelope.message
    -    with pytest.raises(ValueError, match="1~256"):
    -        _ = RawInbound(message_id="x" * 257, message=envelope.message)
    -    assert ChannelDeliveryReceipt(
    -        delivery_id="delivery",
    -        status=DeliveryStatus.UNKNOWN,
    -        error="provider effect uncertain",
    -    ).status is DeliveryStatus.UNKNOWN
    -    assert QueuedReceipt(delivery_id="delivery", queued=True).queued is True
    -
    -    with pytest.raises(ValueError, match="首次 delivery"):
    -        _ = OutboundEnvelope(
    -            logical_delivery_id="logical",
    -            delivery_id="delivery",
    -            attempt_sequence=1,
    -            snapshot_id="snapshot",
    -            generation_id="generation",
    -            binding_token="binding",
    -            channel="feishu",
    -            recipient="chat",
    -            body="hello",
    -            metadata={},
    -        )
    -    with pytest.raises(ValueError, match="新的 delivery_id"):
    -        _ = OutboundEnvelope(
    -            logical_delivery_id="delivery",
    -            delivery_id="delivery",
    -            attempt_sequence=2,
    -            snapshot_id="snapshot",
    -            generation_id="generation",
    -            binding_token="binding",
    -            channel="feishu",
    -            recipient="chat",
    -            body="hello",
    -            metadata={},
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_inbound_handoff_rejects_owner_jump_and_old_owner_close() -> None:
    -    envelope = _inbound_envelope()
    -    with pytest.raises(CompositionError, match="不能从"):
    -        envelope.handoff(InboundOwner.INGRESS, InboundOwner.LANE)
    -
    -    assert envelope.handoff(InboundOwner.INGRESS, InboundOwner.BUS) is envelope
    -    with pytest.raises(CompositionError, match="当前 owner"):
    -        await envelope.close(InboundOwner.INGRESS)
    -
    -    assert envelope.handoff(InboundOwner.BUS, InboundOwner.LANE) is envelope
    -    assert envelope.handoff(InboundOwner.LANE, InboundOwner.LOOP) is envelope
    -    with pytest.raises(CompositionError, match="当前 owner"):
    -        await envelope.close(InboundOwner.BUS)
    -    envelope.lease.release.set()  # type: ignore[attr-defined]
    -    await envelope.close(InboundOwner.LOOP)
    -    with pytest.raises(CompositionError, match="terminal"):
    -        envelope.handoff(InboundOwner.LOOP, InboundOwner.BUS)
    -
    -
    -@pytest.mark.asyncio
    -async def test_inbound_close_is_idempotent_only_for_exact_owner() -> None:
    -    lease = _Lease()
    -    envelope = _inbound_envelope(lease)
    -    lease.release.set()
    -    await envelope.close(InboundOwner.INGRESS)
    -    await envelope.close(InboundOwner.INGRESS)
    -    assert lease.close_calls == 1
    -    assert envelope.owner is InboundOwner.CLOSED
    -    assert envelope.state is InboundState.TERMINAL
    -    with pytest.raises(CompositionError, match="另一 owner"):
    -        await envelope.close(InboundOwner.BUS)
    -
    -
    -@pytest.mark.asyncio
    -async def test_inbound_close_completes_lease_before_propagating_cancellation() -> None:
    -    lease = _Lease()
    -    envelope = _inbound_envelope(lease)
    -    close_task = asyncio.create_task(envelope.close(InboundOwner.INGRESS))
    -    await lease.started.wait()
    -    close_task.cancel()
    -    lease.release.set()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await close_task
    -    assert lease.close_calls == 1
    -    assert envelope.owner is InboundOwner.CLOSED
    -    assert envelope.state is InboundState.TERMINAL
    -    await envelope.close(InboundOwner.INGRESS)
    diff --git a/tests/test_plugin_composition_commands.py b/tests/test_plugin_composition_commands.py
    deleted file mode 100644
    index 6e657737a..000000000
    --- a/tests/test_plugin_composition_commands.py
    +++ /dev/null
    @@ -1,630 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, cast
    -from unittest.mock import AsyncMock, MagicMock
    -
    -import pytest
    -
    -from agent.context import ContextBuilder
    -from agent.core.passive_turn import (
    -    ContextStore,
    -    PassiveTurnDeps,
    -    PassiveTurnPipeline,
    -    Reasoner,
    -)
    -from agent.core.runtime_support import SessionLike
    -from agent.looping.ports import SessionServices
    -from agent.looping.core import AgentLoop
    -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps
    -from agent.plugin_composition import (
    -    COMMANDS,
    -    CommandDefinition,
    -    CommandDescriptor,
    -    CommandRegistry,
    -    CommandResult,
    -    CompositionRoot,
    -    PluginCommands,
    -    PluginRuntime,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import (
    -    RuntimeSnapshotCompiler,
    -    bind_runtime_snapshot,
    -    reset_runtime_snapshot,
    -)
    -from agent.lifecycle.types import BeforeTurnCtx
    -from agent.tools.registry import ToolRegistry
    -from agent.turns.outbound import OutboundPort
    -from bus.event_bus import EventBus
    -from bus.events import InboundMessage, TurnDisposition
    -from bus.queue import MessageBus
    -from tests.memory_fakes import FakeMemoryEngine
    -from tests.provider_fakes import ProviderContextBudgetStub
    -
    -
    -def _write_plugin(root: Path, name: str, source: str) -> Path:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(source, encoding="utf-8")
    -    return plugin_dir
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -def _current_commands(manager: PluginManager) -> tuple[tuple[str, str], ...]:
    -    snapshot = manager.current_snapshot
    -    if snapshot is None or snapshot.command_registry is None:
    -        return ()
    -    return tuple(
    -        (descriptor.name, descriptor.description)
    -        for descriptor in snapshot.command_registry.descriptors
    -    )
    -
    -
    -def _command_plugin(description: str, reply: str) -> str:
    -    return (
    -        "from agent.plugin_composition import COMMANDS, CommandDefinition, CommandResult\n"
    -        "api_version = 3\n"
    -        "name = 'commands_v3'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (COMMANDS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    async def handler(invocation):\n"
    -        f"        return CommandResult('success', {reply!r} + ':' + invocation.raw_input)\n"
    -        "    await ctx.require(COMMANDS).register(ctx, CommandDefinition(\n"
    -        f"        name='hello', description={description!r}, handler=handler,\n"
    -        "        aliases=('hi',), input_hint='name'))\n"
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_commands_execute_alias_and_cleanup(tmp_path: Path) -> None:
    -    root = CompositionRoot("commands")
    -    commands = PluginCommands()
    -    _ = await root.context.provide(COMMANDS, commands)
    -    runtime = PluginRuntime(
    -        plugin_id="command-probe",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "data",
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -
    -    async def plugin(ctx) -> None:
    -        async def handler(invocation):
    -            return CommandResult("success", invocation.raw_input or "empty")
    -
    -        await ctx.require(COMMANDS).register(
    -            ctx,
    -            CommandDefinition(
    -                name="hello",
    -                description="say hello",
    -                handler=handler,
    -                aliases=("hi",),
    -                input_hint="name",
    -            ),
    -        )
    -
    -    _ = await root.mount(plugin, name="command-probe", runtime=runtime)
    -    registry = commands.freeze()
    -
    -    execution = await registry.execute(
    -        "/HI@akashic  花月",
    -        session_key="web:1",
    -        channel="web",
    -        chat_id="1",
    -        sender="hua",
    -    )
    -
    -    assert execution is not None
    -    assert execution.name == "hello"
    -    assert execution.result == CommandResult("success", "  花月")
    -    assert registry.descriptors[0].input_hint == "name"
    -    await root.dispose()
    -    assert root.receipt().effects == ()
    -    assert root.receipt().services == ()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("name", ("bad-name", "a" * 33))
    -async def test_plugin_commands_reject_names_outside_channel_contract(
    -    tmp_path: Path,
    -    name: str,
    -) -> None:
    -    root = CompositionRoot("commands")
    -    commands = PluginCommands()
    -    _ = await root.context.provide(COMMANDS, commands)
    -    runtime = PluginRuntime(
    -        plugin_id="command-probe",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "data",
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.require(COMMANDS).register(
    -            ctx,
    -            CommandDefinition(
    -                name=name,
    -                description="invalid",
    -                handler=lambda _invocation: CommandResult("success", "ok"),
    -            ),
    -        )
    -
    -    fiber = await root.mount(plugin, name="command-probe", runtime=runtime)
    -    assert fiber.state.value == "failed"
    -    assert "Command name 无效" in (root.receipt().fibers[0].error or "")
    -    assert not root.receipt().ready
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("name", "aliases", "match"),
    -    (("stop", (), "Core 保留"), ("hello", ("stop",), "别名由 Core 保留")),
    -)
    -async def test_plugin_commands_reject_core_reserved_names(
    -    tmp_path: Path,
    -    name: str,
    -    aliases: tuple[str, ...],
    -    match: str,
    -) -> None:
    -    root = CompositionRoot("commands")
    -    commands = PluginCommands()
    -    _ = await root.context.provide(COMMANDS, commands)
    -    runtime = PluginRuntime(
    -        plugin_id="command-probe",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "data",
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.require(COMMANDS).register(
    -            ctx,
    -            CommandDefinition(
    -                name=name,
    -                description="reserved",
    -                handler=lambda _invocation: CommandResult("success", "ok"),
    -                aliases=aliases,
    -            ),
    -        )
    -
    -    fiber = await root.mount(plugin, name="command-probe", runtime=runtime)
    -    assert fiber.state.value == "failed"
    -    assert match in (root.receipt().fibers[0].error or "")
    -    assert not root.receipt().ready
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_commands_reject_channel_description_over_256_chars(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("commands")
    -    commands = PluginCommands()
    -    _ = await root.context.provide(COMMANDS, commands)
    -    runtime = PluginRuntime(
    -        plugin_id="command-probe",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "data",
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.require(COMMANDS).register(
    -            ctx,
    -            CommandDefinition(
    -                name="hello",
    -                description="x" * 257,
    -                handler=lambda _invocation: CommandResult("success", "ok"),
    -            ),
    -        )
    -
    -    fiber = await root.mount(plugin, name="command-probe", runtime=runtime)
    -    assert fiber.state.value == "failed"
    -    assert "超过 256 字符" in (root.receipt().fibers[0].error or "")
    -    assert not root.receipt().ready
    -    await root.dispose()
    -
    -
    -def test_command_digest_covers_every_descriptor_field() -> None:
    -    async def handler(_invocation):
    -        return CommandResult("success", "ok")
    -
    -    base = CommandDefinition("hello", "description", handler, ("hi",), "name")
    -
    -    def digest(
    -        definition: CommandDefinition,
    -        *,
    -        owner: str = "plugin-a",
    -    ) -> str:
    -        commands = {definition.name: definition}
    -        owners = {definition.name: owner}
    -        descriptor = (
    -            CommandDescriptor(
    -                definition.name,
    -                definition.description,
    -                definition.aliases,
    -                definition.input_hint,
    -                owner,
    -            ),
    -        )
    -        return CommandRegistry(commands, owners, descriptor).catalog_digest
    -
    -    variants = (
    -        CommandDefinition("other", "description", handler, ("hi",), "name"),
    -        CommandDefinition("hello", "changed", handler, ("hi",), "name"),
    -        CommandDefinition("hello", "description", handler, ("hey",), "name"),
    -        CommandDefinition("hello", "description", handler, ("hi",), "target"),
    -    )
    -
    -    baseline = digest(base)
    -    assert all(digest(item) != baseline for item in variants)
    -    assert digest(base, owner="plugin-b") != baseline
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("name", "description", "aliases", "input_hint", "owner"),
    -    (
    -        ("other", "description", ("hi",), "name", "plugin-a"),
    -        ("hello", "changed", ("hi",), "name", "plugin-a"),
    -        ("hello", "description", ("hey",), "name", "plugin-a"),
    -        ("hello", "description", ("hi",), "target", "plugin-a"),
    -        ("hello", "description", ("hi",), "name", "plugin-b"),
    -    ),
    -)
    -async def test_command_descriptor_fields_change_snapshot_identity(
    -    tmp_path: Path,
    -    name: str,
    -    description: str,
    -    aliases: tuple[str, ...],
    -    input_hint: str,
    -    owner: str,
    -) -> None:
    -    async def build(
    -        *,
    -        command_name: str,
    -        command_description: str,
    -        command_aliases: tuple[str, ...],
    -        command_input_hint: str,
    -        plugin_id: str,
    -    ) -> CompositionRoot:
    -        root = CompositionRoot("commands")
    -        commands = PluginCommands()
    -        _ = await root.context.provide(COMMANDS, commands)
    -        runtime = PluginRuntime(
    -            plugin_id=plugin_id,
    -            generation_id="test-generation",
    -            plugin_dir=tmp_path / "plugin",
    -            data_dir=tmp_path / "data",
    -            workspace=tmp_path,
    -            config=None,
    -        )
    -
    -        async def plugin(ctx) -> None:
    -            await ctx.require(COMMANDS).register(
    -                ctx,
    -                CommandDefinition(
    -                    command_name,
    -                    command_description,
    -                    lambda _invocation: CommandResult("success", "ok"),
    -                    command_aliases,
    -                    command_input_hint,
    -                ),
    -            )
    -
    -        _ = await root.mount(plugin, name="plugin", runtime=runtime)
    -        return root
    -
    -    baseline_root = await build(
    -        command_name="hello",
    -        command_description="description",
    -        command_aliases=("hi",),
    -        command_input_hint="name",
    -        plugin_id="plugin-a",
    -    )
    -    variant_root = await build(
    -        command_name=name,
    -        command_description=description,
    -        command_aliases=aliases,
    -        command_input_hint=input_hint,
    -        plugin_id=owner,
    -    )
    -    compiler = RuntimeSnapshotCompiler()
    -
    -    baseline = compiler.compile({}, composition_root=baseline_root)
    -    variant = compiler.compile({}, composition_root=variant_root)
    -
    -    assert baseline.composition_topology is not None
    -    assert variant.composition_topology is not None
    -    assert (
    -        baseline.composition_topology.identity == variant.composition_topology.identity
    -    )
    -    assert baseline.snapshot_id != variant.snapshot_id
    -    await baseline_root.dispose()
    -    await variant_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_keeps_candidate_commands_private_until_promotion(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "commands_v3",
    -        _command_plugin("old description", "old"),
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    old_snapshot = manager.current_snapshot
    -    assert old_snapshot is not None and old_snapshot.command_registry is not None
    -    assert _current_commands(manager) == (("hello", "old description"),)
    -    endpoint_calls: list[tuple[tuple[str, str], ...]] = []
    -
    -    async def endpoint_switcher(
    -        _old_commands,
    -        new_commands,
    -    ) -> None:
    -        provisional = manager.latest_snapshot
    -        assert provisional is not None and provisional is not old_snapshot
    -        assert manager.current_snapshot is old_snapshot
    -        assert provisional.accepting_leases is False
    -        assert old_snapshot.state == "committed"
    -        assert old_snapshot.accepting_leases is False
    -        assert _current_commands(manager) == (
    -            ("hello", "old description"),
    -        )
    -        with pytest.raises(RuntimeError, match="暂停接收"):
    -            manager.snapshot_store.lease()
    -        endpoint_calls.append(new_commands)
    -
    -    quiesce = AsyncMock()
    -    resume = AsyncMock()
    -    manager.bind_endpoint_switcher(endpoint_switcher)
    -    manager.bind_endpoint_admission(quiesce=quiesce, resume=resume)
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _command_plugin("new description", "new"),
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("commands_v3")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    assert candidate.runtime_snapshot.snapshot_id != old_snapshot.snapshot_id
    -    assert _current_commands(manager) == (("hello", "old description"),)
    -
    -    result = await manager.publish_prepared("commands_v3")
    -
    -    assert result["publication_state"] == "committed"
    -    assert _current_commands(manager) == (("hello", "new description"),)
    -    assert all(
    -        name != "hi" for name, _description in _current_commands(manager)
    -    )
    -    assert endpoint_calls == [(("hello", "new description"),)]
    -    quiesce.assert_not_awaited()
    -    resume.assert_not_awaited()
    -    root = manager.current_snapshot.composition_root
    -    assert root is not None
    -    await manager.terminate_all()
    -    assert root.receipt().effects == ()
    -    assert root.receipt().services == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_command_catalog_failure_restores_old_stable_and_generation(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "commands_v3",
    -        _command_plugin("old description", "old"),
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    old_snapshot = manager.current_snapshot
    -    old_generation = manager.generation("commands_v3")
    -    assert old_snapshot is not None and old_generation is not None
    -    published: list[tuple[tuple[str, str], ...]] = []
    -
    -    async def endpoint_switcher(
    -        _old_commands,
    -        new_commands,
    -    ) -> None:
    -        published.append(new_commands)
    -        if new_commands == (("hello", "new description"),):
    -            raise RuntimeError("telegram publication failed")
    -
    -    manager.bind_endpoint_switcher(endpoint_switcher)
    -    (plugin_dir / "plugin.py").write_text(
    -        _command_plugin("new description", "new"),
    -        encoding="utf-8",
    -    )
    -    assert await manager.prepare_candidate("commands_v3") is not None
    -
    -    with pytest.raises(RuntimeError, match="telegram publication failed"):
    -        await manager.publish_prepared("commands_v3")
    -
    -    assert published == [
    -        (("hello", "new description"),),
    -        (("hello", "old description"),),
    -    ]
    -    assert manager.current_snapshot is old_snapshot
    -    assert manager.generation("commands_v3") is old_generation
    -    assert _current_commands(manager) == (("hello", "old description"),)
    -    lease = manager.snapshot_store.lease()
    -    assert lease.snapshot is old_snapshot
    -    await lease.release()
    -    await manager.terminate_all()
    -
    -
    -class _CommandProvider(ProviderContextBudgetStub):
    -    async def chat(self, **_kwargs):
    -        raise AssertionError("known command must not call the model")
    -
    -
    -@pytest.mark.asyncio
    -async def test_agent_loop_command_precedes_model_session_and_turn_started(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "commands_v3",
    -        _command_plugin("description", "handled"),
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    session_manager = MagicMock()
    -    tools = ToolRegistry()
    -    provider = _CommandProvider()
    -    loop = AgentLoop(
    -        AgentLoopDeps(
    -            bus=MessageBus(),
    -            tools=tools,
    -            session_manager=session_manager,
    -            workspace=tmp_path / "loop-workspace",
    -                context=ContextBuilder(tmp_path),
    -        ),
    -        AgentLoopConfig(),
    -    )
    -    loop.bind_runtime_snapshot_store(manager.snapshot_store)
    -    loop._resolve_model_selection = AsyncMock(  # type: ignore[method-assign]
    -        side_effect=AssertionError("known command must not resolve a model")
    -    )
    -    loop._observe_turn_started = AsyncMock()  # type: ignore[method-assign]
    -
    -    result = await loop._process_with_runtime_admission(
    -        InboundMessage("web", "hua", "1", "/hi Akashic"),
    -        dispatch_outbound=False,
    -    )
    -
    -    assert result.content == "handled: Akashic"
    -    session_manager.get_or_create.assert_not_called()
    -    loop._resolve_model_selection.assert_not_awaited()
    -    loop._observe_turn_started.assert_not_awaited()
    -    await manager.terminate_all()
    -
    -
    -def _passive_pipeline(
    -    *,
    -    session_manager: object,
    -    reasoner: object,
    -    outbound_port: object,
    -) -> PassiveTurnPipeline:
    -    return PassiveTurnPipeline(
    -        PassiveTurnDeps(
    -            session=cast(
    -                SessionServices,
    -                SimpleNamespace(session_manager=session_manager, presence=None),
    -            ),
    -            context_store=cast(
    -                ContextStore,
    -                SimpleNamespace(prepare=AsyncMock()),
    -            ),
    -            context=cast(
    -                ContextBuilder,
    -                SimpleNamespace(render=MagicMock()),
    -            ),
    -            tools=cast(ToolRegistry, SimpleNamespace(set_context=MagicMock())),
    -            reasoner=cast(Reasoner, reasoner),
    -            event_bus=EventBus(),
    -            outbound_port=cast(OutboundPort, outbound_port),
    -        )
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_stable_command_short_circuits_before_session_and_model(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "commands_v3",
    -        _command_plugin("description", "handled"),
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    lease = manager.snapshot_store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    session_manager = SimpleNamespace(get_or_create=MagicMock())
    -    reasoner = SimpleNamespace(run_turn=AsyncMock())
    -    outbound = SimpleNamespace(dispatch=AsyncMock())
    -    pipeline = _passive_pipeline(
    -        session_manager=session_manager,
    -        reasoner=reasoner,
    -        outbound_port=outbound,
    -    )
    -    try:
    -        result = await pipeline.run(
    -            InboundMessage("web", "hua", "1", "/hi Akashic"),
    -            "web:1",
    -        )
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -
    -    assert result.content == "handled: Akashic"
    -    assert result.turn_disposition is TurnDisposition.SHORT_CIRCUITED
    -    session_manager.get_or_create.assert_not_called()
    -    reasoner.run_turn.assert_not_awaited()
    -    outbound.dispatch.assert_awaited_once()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_unknown_command_continues_before_turn_path(tmp_path: Path) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "commands_v3",
    -        _command_plugin("description", "handled"),
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    lease = manager.snapshot_store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    session = cast(SessionLike, SimpleNamespace(key="web:1", messages=[], metadata={}))
    -    session_manager = SimpleNamespace(get_or_create=MagicMock(return_value=session))
    -    reasoner = SimpleNamespace(run_turn=AsyncMock())
    -    outbound = SimpleNamespace(dispatch=AsyncMock())
    -    pipeline = _passive_pipeline(
    -        session_manager=session_manager,
    -        reasoner=reasoner,
    -        outbound_port=outbound,
    -    )
    -
    -    async def abort(ctx):
    -        ctx.abort = True
    -        ctx.abort_reply = "legacy path"
    -        return ctx
    -
    -    pipeline._bus.on(BeforeTurnCtx, abort)  # pyright: ignore[reportPrivateUsage]
    -    try:
    -        result = await pipeline.run(
    -            InboundMessage("web", "hua", "1", "/unknown"),
    -            "web:1",
    -        )
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -
    -    assert result.content == "legacy path"
    -    session_manager.get_or_create.assert_called_once_with("web:1")
    -    reasoner.run_turn.assert_not_awaited()
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_diagnostics.py b/tests/test_plugin_composition_diagnostics.py
    deleted file mode 100644
    index 27a8a3e75..000000000
    --- a/tests/test_plugin_composition_diagnostics.py
    +++ /dev/null
    @@ -1,239 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import logging
    -from pathlib import Path
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.control.context import running_turn_id
    -from agent.plugin_composition import (
    -    CompositionRoot,
    -    PluginRuntime,
    -    SerialEventKey,
    -)
    -from agent.plugin_composition.diagnostics import CorePluginDiagnostics
    -from core.error_context import current_client_message_id, current_session_key
    -
    -RUN = SerialEventKey[str, object]("probe.run")
    -
    -
    -def _fields(record: logging.LogRecord) -> dict[str, object]:
    -    return cast(dict[str, object], getattr(record, "akashic_fields"))
    -
    -
    -def _runtime(tmp_path: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id="probe@builtin",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "data",
    -        workspace=tmp_path / "workspace",
    -        config=None,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_core_boundary_and_plugin_details_share_one_parent_chain(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    root = CompositionRoot("generation-probe")
    -
    -    async def apply(ctx: Any) -> None:
    -        async def listener(_: str) -> None:
    -            with ctx.diagnostics.operation("work.retrieve"):
    -                ctx.diagnostics.measure("candidates", 7)
    -
    -        _ = await ctx.on(RUN, listener)
    -
    -    _ = await root.mount(
    -        apply,
    -        name="probe",
    -        runtime=_runtime(tmp_path),
    -    )
    -    await root.context.serial(RUN, "payload")
    -
    -    records = [_fields(record) for record in caplog.records]
    -    boundary = next(
    -        item
    -        for item in records
    -        if item["event"] == "plugin.operation.start"
    -        and item["operation"] == "event.serial"
    -    )
    -    internal = next(
    -        item
    -        for item in records
    -        if item["event"] == "plugin.operation.start"
    -        and item["operation"] == "work.retrieve"
    -    )
    -    measurement = next(
    -        item for item in records if item["event"] == "plugin.measurement"
    -    )
    -    terminal = next(
    -        item
    -        for item in records
    -        if item["event"] == "plugin.operation.done"
    -        and item["operation_id"] == boundary["operation_id"]
    -    )
    -
    -    assert boundary["plugin_id"] == "probe@builtin"
    -    assert boundary["generation_id"] == "test-generation"
    -    assert boundary["plugin_entrypoint"] == "probe.run"
    -    assert internal["parent_operation_id"] == boundary["operation_id"]
    -    assert measurement["operation_id"] == internal["operation_id"]
    -    assert measurement["measurement"] == "candidates"
    -    assert measurement["measurement_value"] == 7
    -    assert terminal["outcome"] == "success"
    -    assert cast(float, terminal["duration_ms"]) >= 0
    -
    -
    -def test_plugin_measurements_reject_dynamic_or_non_finite_values() -> None:
    -    diagnostics = CorePluginDiagnostics(
    -        plugin_id="probe",
    -        generation_id="generation",
    -        fiber="probe",
    -    )
    -
    -    with pytest.raises(ValueError, match="measurement 无效"):
    -        diagnostics.measure("bad name", 1)
    -    with pytest.raises(ValueError, match="有限数字"):
    -        diagnostics.measure("ratio", float("nan"))
    -    with pytest.raises(TypeError, match="必须是数字"):
    -        diagnostics.measure("enabled", cast(Any, True))
    -    with pytest.raises(ValueError, match="unit 无效"):
    -        diagnostics.measure("latency", 1, unit="milliseconds")
    -
    -    with pytest.raises(ValueError, match="Core 签发"):
    -        with diagnostics.resume(cast(Any, object())):
    -            pass
    -
    -
    -def test_diagnostic_sink_failure_does_not_change_plugin_result(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    diagnostics = CorePluginDiagnostics(
    -        plugin_id="probe",
    -        generation_id="generation",
    -        fiber="probe",
    -    )
    -
    -    def broken_sink(*_: object, **__: object) -> None:
    -        raise OSError("sink unavailable")
    -
    -    monkeypatch.setattr(
    -        "agent.plugin_composition.diagnostics.log_event",
    -        broken_sink,
    -    )
    -
    -    with diagnostics.operation("work"):
    -        diagnostics.measure("items", 1)
    -
    -
    -@pytest.mark.parametrize(
    -    ("error", "terminal", "outcome"),
    -    [
    -        (RuntimeError("broken"), "plugin.operation.error", "error"),
    -        (asyncio.CancelledError(), "plugin.operation.cancelled", "cancelled"),
    -    ],
    -)
    -def test_plugin_operation_records_error_and_cancelled_terminals(
    -    error: BaseException,
    -    terminal: str,
    -    outcome: str,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    diagnostics = CorePluginDiagnostics(
    -        plugin_id="probe",
    -        generation_id="generation",
    -        fiber="probe",
    -    )
    -
    -    with pytest.raises(type(error)):
    -        with diagnostics.operation("work"):
    -            raise error
    -
    -    record = next(
    -        _fields(item)
    -        for item in caplog.records
    -        if _fields(item).get("event") == terminal
    -    )
    -    assert record["outcome"] == outcome
    -    assert record["error_type"] == type(error).__name__
    -
    -
    -def test_capture_resume_preserves_handoff_parent(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    diagnostics = CorePluginDiagnostics(
    -        plugin_id="probe",
    -        generation_id="generation",
    -        fiber="probe",
    -    )
    -
    -    session_token = current_session_key.set("session-probe")
    -    turn_token = running_turn_id.set("turn-probe")
    -    client_token = current_client_message_id.set("client-probe")
    -    try:
    -        with diagnostics.operation("enqueue") as enqueue:
    -            captured = diagnostics.capture()
    -        clear_session_token = current_session_key.set(None)
    -        clear_turn_token = running_turn_id.set("")
    -        clear_client_token = current_client_message_id.set("")
    -        try:
    -            with diagnostics.resume(captured):
    -                with diagnostics.operation("dequeue"):
    -                    pass
    -        finally:
    -            current_client_message_id.reset(clear_client_token)
    -            running_turn_id.reset(clear_turn_token)
    -            current_session_key.reset(clear_session_token)
    -    finally:
    -        current_client_message_id.reset(client_token)
    -        running_turn_id.reset(turn_token)
    -        current_session_key.reset(session_token)
    -
    -    dequeue = next(
    -        _fields(record)
    -        for record in caplog.records
    -        if _fields(record).get("event") == "plugin.operation.start"
    -        and _fields(record).get("operation") == "dequeue"
    -    )
    -    assert dequeue["parent_operation_id"] == enqueue.operation_id
    -    assert dequeue["session_id"] == "session-probe"
    -    assert dequeue["turn_id"] == "turn-probe"
    -    assert dequeue["client_message_id"] == "client-probe"
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_effect_cleanup_uses_same_lifecycle_boundary(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    cleaned: list[bool] = []
    -
    -    async def apply(ctx: Any) -> None:
    -        _ = await ctx.effect(lambda: lambda: cleaned.append(True))
    -
    -    root = CompositionRoot("composition-generation")
    -    fiber = await root.mount(
    -        apply,
    -        name="probe",
    -        runtime=_runtime(tmp_path),
    -    )
    -    await fiber.dispose()
    -
    -    terminal = next(
    -        _fields(record)
    -        for record in caplog.records
    -        if _fields(record).get("event") == "plugin.operation.done"
    -        and _fields(record).get("operation") == "lifecycle.cleanup"
    -    )
    -    assert cleaned == [True]
    -    assert terminal["plugin_id"] == "probe@builtin"
    -    assert terminal["generation_id"] == "test-generation"
    diff --git a/tests/test_plugin_composition_events.py b/tests/test_plugin_composition_events.py
    deleted file mode 100644
    index 708c3af50..000000000
    --- a/tests/test_plugin_composition_events.py
    +++ /dev/null
    @@ -1,864 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from dataclasses import dataclass
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    Bail,
    -    CompositionError,
    -    CompositionRoot,
    -    Effect,
    -    EmitEventKey,
    -    FiberState,
    -    ObserveEventKey,
    -    ParallelEventKey,
    -    SerialEventKey,
    -    ServiceKey,
    -    TransformEventKey,
    -)
    -
    -NOTICE = EmitEventKey[str]("notice")
    -TRANSFORM = SerialEventKey[list[str], str]("transform")
    -OBSERVE = ParallelEventKey[str]("observe")
    -FINAL_OBSERVE = ObserveEventKey[str]("final-observe")
    -DEPENDENCY = ServiceKey[str]("event-dependency")
    -
    -
    -@dataclass(frozen=True, slots=True)
    -class Rewrite:
    -    steps: tuple[str, ...]
    -
    -
    -REWRITE = TransformEventKey("rewrite", Rewrite, "test.rewrite.v1")
    -
    -
    -@pytest.mark.parametrize(
    -    "key",
    -    [NOTICE, TRANSFORM, OBSERVE, REWRITE, FINAL_OBSERVE],
    -)
    -@pytest.mark.asyncio
    -async def test_registration_rejects_non_callable_without_root_mutation(
    -    key: object,
    -) -> None:
    -    root = CompositionRoot("invalid-listener")
    -    before = root.topology_view()
    -    before_effects = root.receipt().effects
    -
    -    with pytest.raises(CompositionError) as caught:
    -        _ = await root.context.on(cast(Any, key), cast(Any, None))
    -
    -    after = root.topology_view()
    -    assert caught.value.code == "INVALID_EVENT_LISTENER"
    -    assert after.identity == before.identity
    -    assert after.composition_revision == before.composition_revision
    -    assert after.listeners == ()
    -    assert root.receipt().effects == before_effects
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_emit_runs_sync_listeners_in_registration_order() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("emit-order")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(NOTICE, lambda payload: observed.append(f"first:{payload}"))
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(NOTICE, lambda payload: observed.append(f"second:{payload}"))
    -
    -    await root.mount(first, name="first")
    -    await root.mount(second, name="second")
    -    root.context.emit(NOTICE, "ready")
    -
    -    assert observed == ["first:ready", "second:ready"]
    -    assert "event:EmitEventKey:notice" in root.receipt().effects[0]
    -
    -
    -@pytest.mark.asyncio
    -async def test_topology_identity_preserves_listener_registration_order() -> None:
    -    async def build(order: tuple[str, str]) -> str:
    -        root = CompositionRoot("event-order-identity")
    -
    -        for owner in order:
    -            async def plugin(ctx) -> None:
    -                await ctx.on(NOTICE, lambda _: None)
    -
    -            await root.mount(plugin, name=owner)
    -        return root.topology_identity()
    -
    -    first_then_second = await build(("first", "second"))
    -    second_then_first = await build(("second", "first"))
    -
    -    assert first_then_second != second_then_first
    -
    -
    -@pytest.mark.asyncio
    -async def test_topology_view_exposes_ordered_listener_revision() -> None:
    -    root = CompositionRoot("event-view")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(NOTICE, lambda _: None)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(NOTICE, lambda _: None)
    -
    -    await root.mount(first, name="first")
    -    await root.mount(second, name="second")
    -    view = root.topology_view()
    -
    -    assert view.identity == root.topology_identity()
    -    assert view.listeners == (
    -        "emit:notice:first",
    -        "emit:notice:second",
    -    )
    -    assert len(view.identity) == 64
    -
    -
    -@pytest.mark.asyncio
    -async def test_listener_remove_and_restore_keeps_hash_but_advances_revision() -> None:
    -    root = CompositionRoot("event-revision")
    -    effects: list[Effect] = []
    -
    -    async def plugin(ctx) -> None:
    -        effects.append(await ctx.on(NOTICE, lambda _: None))
    -
    -    fiber = await root.mount(plugin, name="listener")
    -    compiled = root.topology_view()
    -
    -    await effects.pop().aclose()
    -    removed = root.topology_view()
    -    effects.append(await fiber.context.on(NOTICE, lambda _: None))
    -    restored = root.topology_view()
    -
    -    assert removed.identity != compiled.identity
    -    assert restored.identity == compiled.identity
    -    assert restored.composition_revision == compiled.composition_revision + 2
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_emit_rejects_async_listener_during_registration() -> None:
    -    root = CompositionRoot("emit-async-listener")
    -
    -    async def listener(_: str) -> None:
    -        return None
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.on(NOTICE, listener)
    -
    -    fiber = await root.mount(plugin, name="broken")
    -
    -    assert fiber.state == FiberState.FAILED
    -    assert any(
    -        "ASYNC_LISTENER_ON_EMIT" in incident.message
    -        for incident in root.receipt().incidents
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_emit_rejects_awaitable_returned_by_sync_listener() -> None:
    -    root = CompositionRoot("emit-awaitable-result")
    -
    -    async def delayed() -> None:
    -        return None
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.on(NOTICE, lambda _: delayed())
    -
    -    await root.mount(plugin, name="wrapper")
    -
    -    with pytest.raises(CompositionError) as caught:
    -        root.context.emit(NOTICE, "ready")
    -    assert caught.value.code == "ASYNC_RESULT_FROM_EMIT"
    -
    -
    -@pytest.mark.asyncio
    -async def test_event_name_cannot_change_dispatch_mode_while_registered() -> None:
    -    root = CompositionRoot("event-mode-conflict")
    -    conflicting = SerialEventKey[str, str](NOTICE.name)
    -
    -    async def emit_plugin(ctx) -> None:
    -        await ctx.on(NOTICE, lambda _: None)
    -
    -    async def serial_plugin(ctx) -> None:
    -        await ctx.on(conflicting, lambda _: None)
    -
    -    await root.mount(emit_plugin, name="emit-owner")
    -    fiber = await root.mount(serial_plugin, name="serial-owner")
    -
    -    assert fiber.state == FiberState.FAILED
    -    assert any(
    -        "EVENT_MODE_CONFLICT" in incident.message
    -        for incident in root.receipt().incidents
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_serial_awaits_in_order_and_only_explicit_bail_stops() -> None:
    -    observed: list[str] = []
    -    payload: list[str] = []
    -    root = CompositionRoot("serial-bail")
    -
    -    async def first_handler(value: list[str]) -> None:
    -        await asyncio.sleep(0)
    -        value.append("first")
    -        observed.append("first")
    -
    -    def second_handler(value: list[str]) -> Bail[str]:
    -        value.append("second")
    -        observed.append("second")
    -        return Bail("stop")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(TRANSFORM, first_handler)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(TRANSFORM, second_handler)
    -
    -    async def third(ctx) -> None:
    -        await ctx.on(TRANSFORM, lambda value: observed.append("third"))
    -
    -    await root.mount(first, name="first")
    -    await root.mount(second, name="second")
    -    await root.mount(third, name="third")
    -
    -    result = await root.context.serial(TRANSFORM, payload)
    -
    -    assert result == Bail("stop")
    -    assert payload == ["first", "second"]
    -    assert observed == ["first", "second"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_serial_rejects_implicit_truthy_result() -> None:
    -    root = CompositionRoot("serial-invalid-result")
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.on(TRANSFORM, lambda _: "implicit-stop")
    -
    -    await root.mount(plugin, name="invalid")
    -
    -    with pytest.raises(CompositionError) as caught:
    -        await root.context.serial(TRANSFORM, [])
    -    assert caught.value.code == "INVALID_SERIAL_RESULT"
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_returns_original_without_listeners() -> None:
    -    root = CompositionRoot("transform-empty")
    -    original = Rewrite(("original",))
    -
    -    transformed = await root.context.transform(REWRITE, original)
    -
    -    assert transformed is original
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_chains_sync_and_async_listeners_in_order() -> None:
    -    root = CompositionRoot("transform-order")
    -
    -    def first(value: Rewrite) -> Rewrite:
    -        return Rewrite((*value.steps, "first"))
    -
    -    async def second(value: Rewrite) -> Rewrite:
    -        await asyncio.sleep(0)
    -        return Rewrite((*value.steps, "second"))
    -
    -    async def first_plugin(ctx) -> None:
    -        await ctx.on(REWRITE, first)
    -
    -    async def second_plugin(ctx) -> None:
    -        await ctx.on(REWRITE, second)
    -
    -    await root.mount(first_plugin, name="first")
    -    await root.mount(second_plugin, name="second")
    -
    -    transformed = await root.context.transform(REWRITE, Rewrite(()))
    -
    -    assert transformed == Rewrite(("first", "second"))
    -    assert root.topology_view().listeners == (
    -        "transform:rewrite[test.rewrite.v1]:first",
    -        "transform:rewrite[test.rewrite.v1]:second",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("invalid", [None, Bail("stop"), "wrong-type"])
    -async def test_transform_rejects_implicit_or_wrong_result(invalid: object) -> None:
    -    root = CompositionRoot("transform-invalid")
    -
    -    async def plugin(ctx) -> None:
    -        await ctx.on(REWRITE, lambda _: invalid)
    -
    -    await root.mount(plugin, name="invalid")
    -
    -    with pytest.raises(CompositionError) as caught:
    -        await root.context.transform(REWRITE, Rewrite(()))
    -    assert caught.value.code == "INVALID_TRANSFORM_RESULT"
    -    assert root.receipt().incidents[-1].kind == "transform_failure"
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_failure_stops_chain_and_records_incident() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("transform-failure")
    -
    -    def broken(_: Rewrite) -> Rewrite:
    -        raise RuntimeError("rewrite failed")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(REWRITE, broken)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(REWRITE, lambda value: observed.append("second") or value)
    -
    -    await root.mount(first, name="broken")
    -    await root.mount(second, name="second")
    -
    -    with pytest.raises(RuntimeError, match="rewrite failed"):
    -        await root.context.transform(REWRITE, Rewrite(()))
    -
    -    assert observed == []
    -    incident = root.receipt().incidents[-1]
    -    assert (incident.owner, incident.kind, incident.error_type) == (
    -        "broken",
    -        "transform_failure",
    -        "RuntimeError",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_uses_one_frozen_listener_list_per_dispatch() -> None:
    -    root = CompositionRoot("transform-frozen-list")
    -    observed: list[str] = []
    -    second_fiber = None
    -
    -    async def first_listener(value: Rewrite) -> Rewrite:
    -        assert second_fiber is not None
    -        observed.append("first")
    -        await second_fiber.dispose()
    -        return Rewrite((*value.steps, "first"))
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(REWRITE, first_listener)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(
    -            REWRITE,
    -            lambda value: observed.append("second")
    -            or Rewrite((*value.steps, "second")),
    -        )
    -
    -    await root.mount(first, name="first")
    -    second_fiber = await root.mount(second, name="second")
    -
    -    transformed = await root.context.transform(REWRITE, Rewrite(()))
    -
    -    assert transformed == Rewrite(("first", "second"))
    -    assert observed == ["first", "second"]
    -    assert second_fiber.state == FiberState.DISPOSED
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_runs_every_listener_and_contains_all_failures() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("observe-failures")
    -
    -    def sync_failure(_: str) -> None:
    -        observed.append("sync-failure")
    -        raise ValueError("sync observer failed")
    -
    -    async def async_failure(_: str) -> None:
    -        await asyncio.sleep(0)
    -        observed.append("async-failure")
    -        raise RuntimeError("async observer failed")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, sync_failure)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, async_failure)
    -
    -    async def third(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, lambda value: observed.append(value))
    -
    -    await root.mount(first, name="sync")
    -    await root.mount(second, name="async")
    -    await root.mount(third, name="final")
    -
    -    await root.context.observe(FINAL_OBSERVE, "settled")
    -
    -    assert observed == ["sync-failure", "settled", "async-failure"]
    -    failures = [
    -        (incident.owner, incident.kind, incident.error_type)
    -        for incident in root.receipt().incidents
    -    ]
    -    assert failures == [
    -        ("sync", "observer_failure", "ValueError"),
    -        ("async", "observer_failure", "RuntimeError"),
    -    ]
    -    assert root.receipt().ready is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_contains_failure_with_unprintable_exception() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("observe-unprintable")
    -
    -    class UnprintableError(Exception):
    -        def __str__(self) -> str:
    -            raise RuntimeError("coercion trap")
    -
    -    def broken(_: str) -> None:
    -        raise UnprintableError()
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, broken)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, lambda value: observed.append(value))
    -
    -    await root.mount(first, name="broken")
    -    await root.mount(second, name="final")
    -
    -    await root.context.observe(FINAL_OBSERVE, "settled")
    -
    -    assert observed == ["settled"]
    -    incident = root.receipt().incidents[-1]
    -    assert incident.message == ""
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_caller_cancellation_cancels_and_drains_listeners() -> None:
    -    started = [asyncio.Event(), asyncio.Event()]
    -    cleaned = [asyncio.Event(), asyncio.Event()]
    -    root = CompositionRoot("observe-cancel")
    -
    -    def listener(index: int):
    -        async def run(_: str) -> None:
    -            started[index].set()
    -            try:
    -                await asyncio.Future()
    -            finally:
    -                cleaned[index].set()
    -
    -        return run
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, listener(0))
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, listener(1))
    -
    -    await root.mount(first, name="first")
    -    await root.mount(second, name="second")
    -    dispatch = asyncio.create_task(root.context.observe(FINAL_OBSERVE, "settled"))
    -    await asyncio.gather(*(event.wait() for event in started))
    -    _ = dispatch.cancel()
    -    await asyncio.sleep(0)
    -    _ = dispatch.cancel()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await dispatch
    -    assert all(event.is_set() for event in cleaned)
    -    assert root.receipt().incidents == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_sync_system_exit_closes_unstarted_listener_and_propagates() -> None:
    -    started = asyncio.Event()
    -    created = []
    -    root = CompositionRoot("observe-system-exit")
    -
    -    async def blocking(_: str) -> None:
    -        started.set()
    -        await asyncio.Future()
    -
    -    def create_blocking(value: str):
    -        result = blocking(value)
    -        created.append(result)
    -        return result
    -
    -    def terminate(_: str) -> None:
    -        raise SystemExit(7)
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, create_blocking)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, terminate)
    -
    -    await root.mount(first, name="blocking")
    -    await root.mount(second, name="terminate")
    -
    -    with pytest.raises(SystemExit) as caught:
    -        await root.context.observe(FINAL_OBSERVE, "settled")
    -
    -    assert caught.value.code == 7
    -    assert not started.is_set()
    -    assert len(created) == 1
    -    assert created[0].cr_frame is None
    -    assert root.receipt().incidents == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_cleanup_failure_does_not_mask_system_exit() -> None:
    -    created = []
    -    root = CompositionRoot("observe-cleanup-failure")
    -
    -    class BrokenCloseAwaitable:
    -        def __await__(self):
    -            if False:
    -                yield None
    -            return None
    -
    -        def close(self) -> None:
    -            raise RuntimeError("close failed")
    -
    -    async def blocking(_: str) -> None:
    -        await asyncio.Future()
    -
    -    def create_blocking(value: str):
    -        result = blocking(value)
    -        created.append(result)
    -        return result
    -
    -    def terminate(_: str) -> None:
    -        raise SystemExit(8)
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, lambda _: BrokenCloseAwaitable())
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, create_blocking)
    -
    -    async def third(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, terminate)
    -
    -    await root.mount(first, name="broken-close")
    -    await root.mount(second, name="blocking")
    -    await root.mount(third, name="terminate")
    -
    -    with pytest.raises(SystemExit) as caught:
    -        await root.context.observe(FINAL_OBSERVE, "settled")
    -
    -    assert caught.value.code == 8
    -    assert len(created) == 1
    -    assert created[0].cr_frame is None
    -    incident = root.receipt().incidents[-1]
    -    assert incident.owner == "broken-close"
    -    assert incident.kind == "observer_cleanup_failure"
    -    assert incident.error_type == "RuntimeError"
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_async_system_exit_propagates_after_all_callbacks() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("observe-async-system-exit")
    -
    -    async def terminate(_: str) -> None:
    -        raise SystemExit(9)
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, terminate)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(FINAL_OBSERVE, lambda _: observed.append("later"))
    -
    -    await root.mount(first, name="terminate")
    -    await root.mount(second, name="later")
    -
    -    with pytest.raises(SystemExit) as caught:
    -        await root.context.observe(FINAL_OBSERVE, "settled")
    -
    -    assert caught.value.code == 9
    -    assert observed == ["later"]
    -    assert root.receipt().incidents == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_event_name_cannot_change_payload_contract() -> None:
    -    root = CompositionRoot("transform-contract-conflict")
    -    conflicting = TransformEventKey("rewrite", str, "test.rewrite.v1")
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(REWRITE, lambda value: value)
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(conflicting, lambda value: value)
    -
    -    await root.mount(first, name="rewrite-owner")
    -    fiber = await root.mount(second, name="string-owner")
    -
    -    assert fiber.state == FiberState.FAILED
    -    assert any(
    -        "EVENT_MODE_CONFLICT" in incident.message
    -        for incident in root.receipt().incidents
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_transform_topology_uses_stable_payload_contract_token() -> None:
    -    class CandidateRewrite:
    -        pass
    -
    -    class FormalRewrite:
    -        pass
    -
    -    async def build(payload_type: type[object], token: str):
    -        root = CompositionRoot(f"transform-contract:{token}")
    -        key = TransformEventKey("stable-rewrite", payload_type, token)
    -
    -        async def plugin(ctx) -> None:
    -            await ctx.on(key, lambda value: value)
    -
    -        await root.mount(plugin, name="owner")
    -        return root.topology_view()
    -
    -    candidate = await build(CandidateRewrite, "plugin.rewrite.v1")
    -    formal = await build(FormalRewrite, "plugin.rewrite.v1")
    -    changed = await build(FormalRewrite, "plugin.rewrite.v2")
    -
    -    assert candidate.listeners == formal.listeners == (
    -        "transform:stable-rewrite[plugin.rewrite.v1]:owner",
    -    )
    -    assert candidate.identity == formal.identity
    -    assert changed.listeners != formal.listeners
    -    assert changed.identity != formal.identity
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_starts_together_and_aggregates_all_failures() -> None:
    -    first_started = asyncio.Event()
    -    second_started = asyncio.Event()
    -    release = asyncio.Event()
    -    root = CompositionRoot("parallel-errors")
    -
    -    async def first(_: str) -> None:
    -        first_started.set()
    -        await release.wait()
    -        raise ValueError("first")
    -
    -    async def second(_: str) -> None:
    -        second_started.set()
    -        await release.wait()
    -        raise RuntimeError("second")
    -
    -    async def plugin_a(ctx) -> None:
    -        await ctx.on(OBSERVE, first)
    -
    -    async def plugin_b(ctx) -> None:
    -        await ctx.on(OBSERVE, second)
    -
    -    await root.mount(plugin_a, name="first")
    -    await root.mount(plugin_b, name="second")
    -    dispatch = asyncio.create_task(root.context.parallel(OBSERVE, "event"))
    -    await asyncio.gather(first_started.wait(), second_started.wait())
    -    release.set()
    -
    -    with pytest.raises(BaseExceptionGroup) as caught:
    -        await dispatch
    -    assert {type(error) for error in caught.value.exceptions} == {
    -        ValueError,
    -        RuntimeError,
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_cancellation_drains_every_listener() -> None:
    -    started = [asyncio.Event(), asyncio.Event()]
    -    cleaned = [asyncio.Event(), asyncio.Event()]
    -    root = CompositionRoot("parallel-cancel")
    -
    -    def listener(index: int):
    -        async def run(_: str) -> None:
    -            started[index].set()
    -            try:
    -                await asyncio.Future()
    -            finally:
    -                cleaned[index].set()
    -
    -        return run
    -
    -    async def first(ctx) -> None:
    -        await ctx.on(OBSERVE, listener(0))
    -
    -    async def second(ctx) -> None:
    -        await ctx.on(OBSERVE, listener(1))
    -
    -    await root.mount(first, name="first")
    -    await root.mount(second, name="second")
    -    dispatch = asyncio.create_task(root.context.parallel(OBSERVE, "event"))
    -    await asyncio.gather(*(event.wait() for event in started))
    -    _ = dispatch.cancel()
    -    await asyncio.sleep(0)
    -    _ = dispatch.cancel()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await dispatch
    -    assert all(event.is_set() for event in cleaned)
    -
    -
    -@pytest.mark.asyncio
    -async def test_serial_uses_one_frozen_listener_list_per_dispatch() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("serial-frozen-list")
    -    second_fiber = None
    -
    -    async def first_handler(_: list[str]) -> None:
    -        assert second_fiber is not None
    -        observed.append("first")
    -        await second_fiber.dispose()
    -
    -    async def first_plugin(ctx) -> None:
    -        await ctx.on(TRANSFORM, first_handler)
    -
    -    async def second_plugin(ctx) -> None:
    -        await ctx.on(TRANSFORM, lambda _: observed.append("second"))
    -
    -    await root.mount(first_plugin, name="first")
    -    second_fiber = await root.mount(second_plugin, name="second")
    -
    -    result = await root.context.serial(TRANSFORM, [])
    -
    -    assert result is None
    -    assert observed == ["first", "second"]
    -    assert second_fiber.state == FiberState.DISPOSED
    -
    -
    -@pytest.mark.asyncio
    -async def test_dependency_loss_removes_listener_and_restore_registers_once() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("event-dependency")
    -
    -    class Consumer:
    -        name = "consumer"
    -        inject = (DEPENDENCY,)
    -
    -        async def apply(self, ctx) -> None:
    -            await ctx.on(NOTICE, lambda payload: observed.append(payload))
    -
    -    class Provider:
    -        name = "provider"
    -        inject = ()
    -
    -        async def apply(self, ctx) -> None:
    -            await ctx.provide(DEPENDENCY, "ready")
    -
    -    consumer_plugin = Consumer()
    -    consumer = await root.mount(
    -        consumer_plugin.apply,
    -        name=consumer_plugin.name,
    -        inject=consumer_plugin.inject,
    -    )
    -    provider_plugin = Provider()
    -    provider = await root.mount(provider_plugin.apply, name=provider_plugin.name)
    -    root.context.emit(NOTICE, "first")
    -
    -    await provider.dispose()
    -    assert consumer.state == FiberState.PENDING
    -    root.context.emit(NOTICE, "missing")
    -
    -    replacement = Provider()
    -    await root.mount(replacement.apply, name="replacement")
    -    root.context.emit(NOTICE, "second")
    -
    -    assert observed == ["first", "second"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_spawned_task_is_cancelled_with_owning_fiber() -> None:
    -    started = asyncio.Event()
    -    cleaned = asyncio.Event()
    -    root = CompositionRoot("spawn-cleanup")
    -
    -    async def worker() -> None:
    -        started.set()
    -        try:
    -            await asyncio.Future()
    -        finally:
    -            cleaned.set()
    -
    -    async def plugin(ctx) -> None:
    -        _ = await ctx.spawn(worker(), name="worker")
    -
    -    fiber = await root.mount(plugin, name="task-owner")
    -    await started.wait()
    -    assert "task-owner:task:worker" in root.receipt().effects
    -    await fiber.dispose()
    -
    -    assert cleaned.is_set()
    -    assert fiber.effects == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_spawn_rejection_closes_unowned_coroutine() -> None:
    -    contexts: list[Any] = []
    -    root = CompositionRoot("spawn-rejected")
    -
    -    async def plugin(ctx) -> None:
    -        contexts.append(ctx)
    -
    -    fiber = await root.mount(plugin, name="task-owner")
    -    await fiber.dispose()
    -
    -    async def worker() -> None:
    -        await asyncio.sleep(0)
    -
    -    coroutine = worker()
    -    assert coroutine.cr_frame is not None
    -    with pytest.raises(CompositionError) as caught:
    -        _ = await contexts[0].spawn(coroutine, name="late-worker")
    -
    -    assert caught.value.code == "INACTIVE_EFFECT"
    -    assert coroutine.cr_frame is None
    -    assert fiber.effects == []
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_spawned_task_failure_is_visible_to_candidate_readiness() -> None:
    -    failed = asyncio.Event()
    -    recovered = asyncio.Event()
    -    attempts = 0
    -    root = CompositionRoot("spawn-failure")
    -
    -    async def worker() -> None:
    -        nonlocal attempts
    -        attempts += 1
    -        if attempts == 1:
    -            failed.set()
    -            raise RuntimeError("background failed")
    -        recovered.set()
    -        await asyncio.Event().wait()
    -
    -    async def plugin(ctx) -> None:
    -        _ = await ctx.spawn(worker(), name="broken-worker")
    -
    -    fiber = await root.mount(plugin, name="task-owner")
    -    await failed.wait()
    -    await asyncio.sleep(0)
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is False
    -    assert receipt.required_degraded == ("task-owner:task:broken-worker",)
    -    assert any(
    -        incident.kind == "task_failure"
    -        and "background failed" in incident.message
    -        for incident in receipt.incidents
    -    )
    -
    -    await fiber.restart()
    -    await recovered.wait()
    -    recovered_receipt = root.receipt()
    -    assert recovered_receipt.ready is True
    -    assert recovered_receipt.required_degraded == ()
    -    assert any(
    -        "background failed" in incident.message
    -        for incident in recovered_receipt.incidents
    -    )
    -    await root.dispose()
    diff --git a/tests/test_plugin_composition_executor.py b/tests/test_plugin_composition_executor.py
    deleted file mode 100644
    index 276e388e9..000000000
    --- a/tests/test_plugin_composition_executor.py
    +++ /dev/null
    @@ -1,212 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import threading
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    EXECUTOR_SERVICE,
    -    CompositionError,
    -    CompositionRoot,
    -    ExecutorService,
    -    Fiber,
    -    HealthHandle,
    -    SyncTask,
    -)
    -
    -
    -async def _mount_executor(root: CompositionRoot, max_workers: int) -> Fiber:
    -    service = ExecutorService(max_workers=max_workers)
    -    return await root.mount(service.apply, name=service.name)
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_runs_concurrently_and_preserves_result_order() -> None:
    -    barrier = threading.Barrier(2)
    -    root = CompositionRoot("executor-order")
    -    await _mount_executor(root, 2)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    def run(value: str) -> str:
    -        _ = barrier.wait(timeout=1)
    -        return value
    -
    -    results = await executor.parallel_sync(
    -        (
    -            SyncTask("first", lambda: run("first")),
    -            SyncTask("second", lambda: run("second")),
    -        )
    -    )
    -
    -    assert results == ("first", "second")
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_waits_all_and_aggregates_errors() -> None:
    -    completed: list[str] = []
    -    root = CompositionRoot("executor-errors")
    -    await _mount_executor(root, 2)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    def fail(name: str, error: Exception) -> None:
    -        completed.append(name)
    -        raise error
    -
    -    with pytest.raises(BaseExceptionGroup) as caught:
    -        await executor.parallel_sync(
    -            (
    -                SyncTask("first", lambda: fail("first", ValueError("first"))),
    -                SyncTask("second", lambda: fail("second", RuntimeError("second"))),
    -            )
    -        )
    -
    -    assert sorted(completed) == ["first", "second"]
    -    assert {type(error) for error in caught.value.exceptions} == {
    -        ValueError,
    -        RuntimeError,
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_worker_cannot_access_context() -> None:
    -    root = CompositionRoot("executor-context-boundary")
    -    await _mount_executor(root, 1)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    with pytest.raises(BaseExceptionGroup) as caught:
    -        await executor.parallel_sync(
    -            (SyncTask("escape", lambda: root.context.generation_id),)
    -        )
    -
    -    error = caught.value.exceptions[0]
    -    assert isinstance(error, CompositionError)
    -    assert error.code == "CONTEXT_IN_SYNC_WORKER"
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_worker_cannot_mutate_saved_health_handle() -> None:
    -    root = CompositionRoot("executor-health-boundary")
    -    await _mount_executor(root, 1)
    -    handles: list[HealthHandle] = []
    -
    -    async def plugin(ctx) -> None:
    -        handles.append(await ctx.health("worker", required=True))
    -
    -    await root.mount(plugin, name="plugin")
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    with pytest.raises(BaseExceptionGroup) as caught:
    -        await executor.parallel_sync(
    -            (SyncTask("escape", lambda: handles[0].degrade("thread write")),)
    -        )
    -
    -    error = caught.value.exceptions[0]
    -    assert isinstance(error, CompositionError)
    -    assert error.code == "CONTEXT_IN_SYNC_WORKER"
    -    assert root.receipt().required_degraded == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_worker_cannot_read_saved_fiber_handle() -> None:
    -    root = CompositionRoot("executor-fiber-boundary")
    -    await _mount_executor(root, 1)
    -    handle = await root.context.mount(lambda _: None, name="plugin")
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    with pytest.raises(BaseExceptionGroup) as caught:
    -        await executor.parallel_sync(
    -            (SyncTask("escape", lambda: handle.state),)
    -        )
    -
    -    error = caught.value.exceptions[0]
    -    assert isinstance(error, CompositionError)
    -    assert error.code == "CONTEXT_IN_SYNC_WORKER"
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_cancellation_joins_running_thread() -> None:
    -    started = threading.Event()
    -    release = threading.Event()
    -    root = CompositionRoot("executor-cancel")
    -    await _mount_executor(root, 1)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    def run() -> str:
    -        started.set()
    -        _ = release.wait(timeout=2)
    -        return "done"
    -
    -    call = asyncio.create_task(
    -        executor.parallel_sync((SyncTask("running", run),))
    -    )
    -    assert await asyncio.to_thread(started.wait, 1)
    -    _ = call.cancel()
    -    await asyncio.sleep(0)
    -    assert call.done() is False
    -    _ = call.cancel()
    -    await asyncio.sleep(0)
    -    assert call.done() is False
    -    release.set()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await call
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_cancellation_drops_queued_task() -> None:
    -    started = threading.Event()
    -    release = threading.Event()
    -    queued_ran = threading.Event()
    -    root = CompositionRoot("executor-cancel-queued")
    -    await _mount_executor(root, 1)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    def running() -> str:
    -        started.set()
    -        _ = release.wait(timeout=2)
    -        return "running"
    -
    -    def queued() -> str:
    -        queued_ran.set()
    -        return "queued"
    -
    -    call = asyncio.create_task(
    -        executor.parallel_sync(
    -            (
    -                SyncTask("running", running),
    -                SyncTask("queued", queued),
    -            )
    -        )
    -    )
    -    assert await asyncio.to_thread(started.wait, 1)
    -    _ = call.cancel()
    -    await asyncio.sleep(0)
    -    release.set()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await call
    -    assert queued_ran.is_set() is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_executor_provider_dispose_closes_pool_and_removes_service() -> None:
    -    root = CompositionRoot("executor-dispose")
    -    provider = await _mount_executor(root, 1)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    await provider.dispose()
    -
    -    assert root.context.get(EXECUTOR_SERVICE) is None
    -    with pytest.raises(CompositionError) as caught:
    -        await executor.parallel_sync((SyncTask("late", lambda: "late"),))
    -    assert caught.value.code == "EXECUTOR_CLOSED"
    -
    -
    -@pytest.mark.asyncio
    -async def test_parallel_sync_empty_batch_is_valid() -> None:
    -    root = CompositionRoot("executor-empty")
    -    await _mount_executor(root, 1)
    -    executor = root.context.require(EXECUTOR_SERVICE)
    -
    -    assert await executor.parallel_sync(()) == ()
    diff --git a/tests/test_plugin_composition_experiment.py b/tests/test_plugin_composition_experiment.py
    deleted file mode 100644
    index 017586d69..000000000
    --- a/tests/test_plugin_composition_experiment.py
    +++ /dev/null
    @@ -1,88 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import os
    -import subprocess
    -import sys
    -from pathlib import Path
    -
    -
    -def test_experiment_runs_full_candidate_promotion_in_isolated_workspace(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            "scripts/plugin_composition_experiment.py",
    -            "--workspace",
    -            str(workspace),
    -        ],
    -        cwd=Path(__file__).resolve().parents[1],
    -        text=True,
    -        capture_output=True,
    -        check=False,
    -        timeout=30,
    -    )
    -    assert result.returncode == 0, result.stderr
    -
    -    result_path = Path(result.stdout.strip())
    -    evidence = json.loads(result_path.read_text(encoding="utf-8"))
    -    assert evidence["workspace"] == str(workspace)
    -    assert evidence["observed_signal"] == "first"
    -    assert evidence["promoted_signal"] == "second"
    -    assert evidence["receipts"]["pending"]["ready"] is False
    -    assert evidence["receipts"]["ready"]["ready"] is True
    -    assert evidence["receipts"]["removed"]["ready"] is False
    -    assert evidence["receipts"]["restored"]["ready"] is True
    -    assert evidence["receipts"]["promoted"]["ready"] is True
    -    assert evidence["receipts"]["disposed"]["ready"] is False
    -    assert evidence["receipts"]["restored"]["external_effects"] == []
    -    assert evidence["receipts"]["restored"]["writes"] == []
    -    state = json.loads(
    -        (workspace / "plugin-data/probe-provider/state.json").read_text(
    -            encoding="utf-8"
    -        )
    -    )
    -    assert state["value"] == "second"
    -
    -
    -def test_experiment_refuses_an_existing_workspace(tmp_path: Path) -> None:
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            "scripts/plugin_composition_experiment.py",
    -            "--workspace",
    -            str(tmp_path),
    -        ],
    -        cwd=Path(__file__).resolve().parents[1],
    -        text=True,
    -        capture_output=True,
    -        check=False,
    -        timeout=30,
    -    )
    -    assert result.returncode != 0
    -    assert "实验 workspace 必须尚不存在" in result.stderr
    -
    -
    -def test_experiment_refuses_child_of_formal_workspace(tmp_path: Path) -> None:
    -    formal_workspace = tmp_path / "formal"
    -    formal_workspace.mkdir()
    -    environment = dict(os.environ)
    -    environment["AKASHIC_WORKSPACE"] = str(formal_workspace)
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            "scripts/plugin_composition_experiment.py",
    -            "--workspace",
    -            str(formal_workspace / "candidate"),
    -        ],
    -        cwd=Path(__file__).resolve().parents[1],
    -        env=environment,
    -        text=True,
    -        capture_output=True,
    -        check=False,
    -        timeout=30,
    -    )
    -    assert result.returncode != 0
    -    assert "不能位于正式状态根内" in result.stderr
    diff --git a/tests/test_plugin_composition_generation_host.py b/tests/test_plugin_composition_generation_host.py
    deleted file mode 100644
    index 906535e3a..000000000
    --- a/tests/test_plugin_composition_generation_host.py
    +++ /dev/null
    @@ -1,191 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import socket
    -import sys
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    MANAGED_PROCESSES,
    -    MCP_SERVERS,
    -    CompositionRoot,
    -    EndpointEnv,
    -    ManagedProcessDefinition,
    -    McpServerDefinition,
    -    PluginRuntime,
    -)
    -from agent.plugin_composition.mcp_slots import PluginMcpServers
    -from agent.plugin_composition.process_slots import PluginManagedProcesses
    -from agent.plugins.composition_generation_host import CompositionGenerationHost
    -from agent.plugins.generation import GateResult, PluginContributions, PluginGeneration
    -from agent.plugins.scope import PluginScope
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler
    -from agent.tools.registry import ToolRegistry
    -
    -
    -def _free_port() -> int:
    -    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
    -        listener.bind(("127.0.0.1", 0))
    -        return int(listener.getsockname()[1])
    -
    -
    -def _port_live(port: int) -> bool:
    -    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
    -        return probe.connect_ex(("127.0.0.1", port)) == 0
    -
    -
    -def _write_http_server(path: Path) -> None:
    -    path.write_text(
    -        "import os\n"
    -        "from http.server import BaseHTTPRequestHandler, HTTPServer\n"
    -        "class Handler(BaseHTTPRequestHandler):\n"
    -        "    def do_GET(self):\n"
    -        "        self.send_response(200); self.end_headers(); self.wfile.write(b'ready')\n"
    -        "    def log_message(self, *_args): pass\n"
    -        "HTTPServer(('127.0.0.1', int(os.environ['PORT'])), Handler).serve_forever()\n",
    -        encoding="utf-8",
    -    )
    -
    -
    -def _write_mcp_server(path: Path) -> None:
    -    path.write_text(
    -        "import json, os, sys\n"
    -        "for raw in sys.stdin:\n"
    -        "    msg = json.loads(raw); method = msg.get('method')\n"
    -        "    if method == 'initialize': result = {'protocolVersion': '2025-11-25'}\n"
    -        "    elif method == 'tools/list': result = {'tools': [{'name': 'read', "
    -        "'description': 'read env', 'inputSchema': {'type': 'object'}}]}\n"
    -        "    elif method == 'tools/call':\n"
    -        "        result = {'content': [{'type': 'text', 'text': '|'.join((\n"
    -        "            os.environ['ROLE'], os.environ['PORT'],\n"
    -        "            os.environ['AKA_PLUGIN_DATA_DIR'], os.environ['AKASHIC_WORKSPACE'],\n"
    -        "        ))}]}\n"
    -        "    else: continue\n"
    -        "    print(json.dumps({'jsonrpc': '2.0', 'id': msg['id'], 'result': result}), flush=True)\n",
    -        encoding="utf-8",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_exact_root_candidate_materializes_process_mcp_and_tool_route(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = tmp_path / "calendar"
    -    plugin_dir.mkdir()
    -    process_script = plugin_dir / "process.py"
    -    mcp_script = plugin_dir / "mcp.py"
    -    _write_http_server(process_script)
    -    _write_mcp_server(mcp_script)
    -    data_dir = tmp_path / "validation-data"
    -    workspace = tmp_path / "validation-workspace"
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -
    -    root = CompositionRoot("runtime-host")
    -    process_declarations = PluginManagedProcesses(root.instance_token)
    -    mcp_declarations = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MANAGED_PROCESSES, process_declarations)
    -    _ = await root.context.provide(MCP_SERVERS, mcp_declarations)
    -    formal_port = _free_port()
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(MANAGED_PROCESSES).register(
    -            ctx,
    -            ManagedProcessDefinition(
    -                name="calendar_api",
    -                command=("python", "process.py"),
    -                cwd=".",
    -                formal_port=formal_port,
    -                readiness_path="/health",
    -            ),
    -        )
    -        await ctx.require(MCP_SERVERS).register(
    -            ctx,
    -            McpServerDefinition(
    -                name="calendar",
    -                command=("python", "mcp.py"),
    -                cwd=".",
    -                required_tools=("read",),
    -                candidate_read_only_tools=("read",),
    -                endpoint_env=(EndpointEnv("PORT", "calendar_api"),),
    -                candidate_env={"ROLE": "recording"},
    -            ),
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MANAGED_PROCESSES, MCP_SERVERS),
    -        runtime=PluginRuntime(
    -            plugin_id="calendar",
    -            generation_id="test-generation",
    -            plugin_dir=plugin_dir,
    -            data_dir=data_dir,
    -            workspace=workspace,
    -            config=None,
    -        ),
    -    )
    -    generation = PluginGeneration(
    -        plugin_id="calendar",
    -        generation_id="calendar:test",
    -        module_path="plugins.calendar",
    -        source_revision="source",
    -        config_revision="config",
    -        plugin_dir=plugin_dir,
    -        data_dir=data_dir,
    -        config=None,
    -        instance=object(),
    -        scope=PluginScope("calendar"),
    -        contributions=PluginContributions(manifest={}),
    -        gate_result=GateResult(
    -            gate_id="gate",
    -            plugin_id="calendar",
    -            candidate_revision="source",
    -            status="passed",
    -            checks=(),
    -        ),
    -        static_runtime_commands=(
    -            ("mcp:calendar", (sys.executable, str(mcp_script))),
    -            ("process:calendar_api", (sys.executable, str(process_script))),
    -        ),
    -    )
    -    snapshot = RuntimeSnapshotCompiler().compile(
    -        {generation.plugin_id: generation},
    -        composition_root=root,
    -    )
    -    snapshot.tool_registry = ToolRegistry(follow_runtime_snapshot=False)
    -    host = CompositionGenerationHost()
    -
    -    try:
    -        runtime = await host.start(
    -            generation,
    -            snapshot,
    -            mode="candidate",
    -        )
    -        assert runtime is not None and runtime.processes is not None
    -        endpoint = runtime.processes.endpoint("calendar_api")
    -        assert endpoint.port != formal_port
    -        assert _port_live(endpoint.port)
    -        assert root.receipt().ready
    -
    -        registry = host.attach_tools(snapshot.tool_registry, runtime)
    -        assert registry is snapshot.tool_registry and registry is not None
    -        tool = registry.get_tool("mcp_calendar__read")
    -        assert tool is not None
    -        output = await tool.execute()
    -        assert output == "|".join(
    -            ("recording", str(endpoint.port), str(data_dir), str(workspace))
    -        )
    -    finally:
    -        await host.stop(generation.generation_id)
    -        await root.dispose()
    -        _ = await generation.scope.aclose()
    -
    -    assert not _port_live(formal_port)
    -    assert not any(
    -        task.get_name().startswith(("mcp_generation_", "managed_process_"))
    -        for task in asyncio.all_tasks()
    -        if not task.done()
    -    )
    diff --git a/tests/test_plugin_composition_kernel.py b/tests/test_plugin_composition_kernel.py
    deleted file mode 100644
    index d56d8825a..000000000
    --- a/tests/test_plugin_composition_kernel.py
    +++ /dev/null
    @@ -1,1513 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from collections.abc import Callable
    -from dataclasses import asdict
    -from pathlib import Path
    -
    -import pytest
    -
    -import agent.plugin_composition as plugin_composition
    -
    -from agent.plugin_composition import (
    -    CompositionAudit,
    -    CompositionError,
    -    CompositionOverlay,
    -    CompositionRoot,
    -    EmitEventKey,
    -    Fiber,
    -    FiberState,
    -    HealthHandle,
    -    ParallelEventKey,
    -    PluginRuntime,
    -    ServiceKey,
    -)
    -from agent.plugin_composition.access import ExternalEffectGate, PluginDataAccess
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler, RuntimeSnapshotStore
    -from agent.plugins.manager import PluginManager
    -
    -GREETING = ServiceKey[str]("greeting")
    -FORMATTER = ServiceKey[Callable[[str], str]]("formatter")
    -
    -
    -def _runtime(tmp_path: Path, plugin_id: str) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id=plugin_id,
    -        generation_id=f"generation:{plugin_id}",
    -        plugin_dir=tmp_path / "plugins" / plugin_id,
    -        data_dir=tmp_path / "plugin-data" / plugin_id,
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -
    -
    -class GreetingProvider:
    -    name = "greeting-provider"
    -    inject = ()
    -
    -    def __init__(self, value: str = "hello") -> None:
    -        self.value = value
    -
    -    async def apply(self, ctx) -> None:
    -        await ctx.provide(GREETING, self.value)
    -
    -
    -async def _mount_greeting(
    -    root: CompositionRoot,
    -    value: str = "hello",
    -    *,
    -    name: str = "greeting-provider",
    -) -> Fiber:
    -    plugin = GreetingProvider(value)
    -    return await root.mount(plugin.apply, name=name)
    -
    -
    -@pytest.mark.asyncio
    -async def test_overlay_topology_matches_formal_event_key_order(tmp_path: Path) -> None:
    -    first = EmitEventKey[str]("fixture.first")
    -    second = EmitEventKey[str]("fixture.second")
    -
    -    async def mount(root: CompositionRoot, plugin_id: str, keys) -> None:
    -        async def apply(ctx) -> None:
    -            for key in keys:
    -                await ctx.on(key, lambda _payload: None)
    -
    -        _ = await root.mount(
    -            apply,
    -            name=plugin_id,
    -            runtime=_runtime(tmp_path, plugin_id),
    -        )
    -
    -    stable = CompositionRoot("stable")
    -    candidate = CompositionRoot("candidate")
    -    formal = CompositionRoot("formal")
    -    await mount(stable, "a", (first, second))
    -    await mount(stable, "b", (second, first))
    -    await mount(candidate, "b", (second, first))
    -    await mount(formal, "a", (first, second))
    -    await mount(formal, "b", (second, first))
    -    overlay = CompositionOverlay(
    -        stable,
    -        candidate,
    -        plugin_ids=frozenset({"a", "b"}),
    -        replaced_plugin_ids=frozenset({"b"}),
    -    )
    -
    -    assert overlay.topology_view().listeners == formal.topology_view().listeners
    -    assert overlay.topology_identity() == formal.topology_identity()
    -
    -    await overlay.dispose()
    -    await stable.dispose()
    -    await formal.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_overlay_keeps_unchanged_stable_listener_order(tmp_path: Path) -> None:
    -    event = EmitEventKey[str]("fixture.shared")
    -
    -    async def mount(root: CompositionRoot, plugin_id: str, *, listen: bool) -> None:
    -        async def apply(ctx) -> None:
    -            if listen:
    -                await ctx.on(event, lambda _payload: None)
    -
    -        _ = await root.mount(
    -            apply,
    -            name=plugin_id,
    -            runtime=_runtime(tmp_path, plugin_id),
    -        )
    -
    -    stable = CompositionRoot("stable")
    -    candidate = CompositionRoot("candidate")
    -    formal = CompositionRoot("formal")
    -    for root in (stable, formal):
    -        await mount(root, "models", listen=True)
    -        await mount(root, "akasha", listen=True)
    -    await mount(candidate, "restart_probe", listen=False)
    -    await mount(formal, "restart_probe", listen=False)
    -    overlay = CompositionOverlay(
    -        stable,
    -        candidate,
    -        plugin_ids=frozenset({"akasha", "models", "restart_probe"}),
    -        replaced_plugin_ids=frozenset({"restart_probe"}),
    -    )
    -
    -    assert overlay.topology_view().listeners == formal.topology_view().listeners
    -    assert overlay.topology_identity() == formal.topology_identity()
    -
    -    await overlay.dispose()
    -    await stable.dispose()
    -    await formal.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_overlay_keeps_candidate_parallel_listeners_concurrent(
    -    tmp_path: Path,
    -) -> None:
    -    event = ParallelEventKey[None]("fixture.parallel")
    -    both_started = asyncio.Event()
    -    started = 0
    -
    -    async def mount(root: CompositionRoot, plugin_id: str) -> None:
    -        async def listener(_payload: None) -> None:
    -            nonlocal started
    -            started += 1
    -            if started == 2:
    -                both_started.set()
    -            await asyncio.wait_for(both_started.wait(), timeout=0.2)
    -
    -        async def apply(ctx) -> None:
    -            await ctx.on(event, listener)
    -
    -        _ = await root.mount(
    -            apply,
    -            name=plugin_id,
    -            runtime=_runtime(tmp_path, plugin_id),
    -        )
    -
    -    stable = CompositionRoot("stable")
    -    candidate = CompositionRoot("candidate")
    -    await mount(candidate, "a")
    -    await mount(candidate, "b")
    -    overlay = CompositionOverlay(
    -        stable,
    -        candidate,
    -        plugin_ids=frozenset({"a", "b"}),
    -        replaced_plugin_ids=frozenset({"a", "b"}),
    -    )
    -
    -    await overlay.context.parallel(event, None)
    -
    -    assert started == 2
    -    await overlay.dispose()
    -    await stable.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_overlay_event_dispatch_only_reaches_candidate_plugins(
    -    tmp_path: Path,
    -) -> None:
    -    stable_events: list[str] = []
    -    candidate_events: list[str] = []
    -
    -    async def mount_lifecycle(
    -        root: CompositionRoot,
    -        plugin_id: str,
    -        events: list[str],
    -    ) -> None:
    -        from agent.plugin_composition import RUNTIME_STARTED, RUNTIME_STOPPING
    -
    -        async def apply(ctx) -> None:
    -            await ctx.on(RUNTIME_STARTED, lambda _event: events.append("started"))
    -            await ctx.on(RUNTIME_STOPPING, lambda _event: events.append("stopping"))
    -
    -        _ = await root.mount(
    -            apply,
    -            name=plugin_id,
    -            runtime=_runtime(tmp_path, plugin_id),
    -        )
    -
    -    stable = CompositionRoot("stable")
    -    candidate = CompositionRoot("candidate")
    -    await mount_lifecycle(stable, "stable", stable_events)
    -    await mount_lifecycle(candidate, "candidate", candidate_events)
    -    overlay = CompositionOverlay(
    -        stable,
    -        candidate,
    -        plugin_ids=frozenset({"stable", "candidate"}),
    -        replaced_plugin_ids=frozenset({"candidate"}),
    -    )
    -    from agent.plugin_composition import (
    -        RUNTIME_STARTED,
    -        RUNTIME_STOPPING,
    -        RuntimeStarted,
    -        RuntimeStopping,
    -    )
    -
    -    assert await overlay.context.serial(RUNTIME_STARTED, RuntimeStarted()) is None
    -    assert await overlay.context.serial(RUNTIME_STOPPING, RuntimeStopping()) is None
    -
    -    assert stable_events == []
    -    assert candidate_events == ["started", "stopping"]
    -    await overlay.dispose()
    -    await stable.dispose()
    -
    -
    -def test_internal_access_helpers_are_not_v3_public_exports() -> None:
    -    assert not hasattr(plugin_composition, "ExternalEffectGate")
    -    assert not hasattr(plugin_composition, "PluginDataAccess")
    -    assert not hasattr(plugin_composition, "ScopedPluginData")
    -
    -
    -@pytest.mark.asyncio
    -async def test_data_root_is_core_assigned_and_shared_by_nested_fibers(tmp_path) -> None:
    -    data_root = tmp_path / "plugin-data" / "probe-builtin"
    -    data_root.mkdir(parents=True)
    -    runtime = PluginRuntime(
    -        plugin_id="probe@builtin",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=data_root,
    -        workspace=tmp_path,
    -        config=None,
    -    )
    -    observed = []
    -
    -    async def child(ctx) -> None:
    -        observed.append(ctx.data_root)
    -
    -    async def parent(ctx) -> None:
    -        observed.append(ctx.data_root)
    -        _ = await ctx.mount(child, name="child")
    -
    -    root = CompositionRoot("data-root")
    -    _ = await root.mount(parent, name="parent", runtime=runtime)
    -
    -    assert observed == [data_root, data_root]
    -
    -
    -@pytest.mark.asyncio
    -async def test_workspace_root_is_declared_and_shared_by_nested_fibers(tmp_path) -> None:
    -    memes = tmp_path / "memes"
    -    memes.mkdir()
    -    runtime = PluginRuntime(
    -        plugin_id="probe@builtin",
    -        generation_id="test-generation",
    -        plugin_dir=tmp_path / "plugin",
    -        data_dir=tmp_path / "plugin-data" / "probe-builtin",
    -        workspace=tmp_path,
    -        config=None,
    -        workspace_roots=("memes",),
    -    )
    -    observed = []
    -
    -    async def child(ctx) -> None:
    -        observed.append(ctx.workspace_root("memes"))
    -
    -    async def parent(ctx) -> None:
    -        observed.append(ctx.workspace_root("memes"))
    -        with pytest.raises(CompositionError) as caught:
    -            _ = ctx.workspace_root("attachments")
    -        assert caught.value.code == "WORKSPACE_ROOT_UNDECLARED"
    -        _ = await ctx.mount(child, name="child")
    -
    -    root = CompositionRoot("workspace-root")
    -    _ = await root.mount(parent, name="parent", runtime=runtime)
    -
    -    assert observed == [memes.resolve(), memes.resolve()]
    -
    -
    -def test_data_root_requires_core_assigned_plugin_runtime() -> None:
    -    root = CompositionRoot("missing-data-root")
    -
    -    with pytest.raises(CompositionError) as caught:
    -        _ = root.context.data_root
    -
    -    assert caught.value.code == "PLUGIN_RUNTIME_UNAVAILABLE"
    -
    -
    -@pytest.mark.asyncio
    -async def test_public_mount_rejects_object_apply_abi() -> None:
    -    class LegacyPlugin:
    -        async def apply(self, _ctx) -> None:
    -            return None
    -
    -    root = CompositionRoot("callable-only")
    -    legacy = LegacyPlugin()
    -    with pytest.raises(TypeError, match="callable"):
    -        await root.mount(legacy)  # type: ignore[arg-type]
    -
    -    async def parent(ctx) -> None:
    -        with pytest.raises(TypeError, match="child callable"):
    -            await ctx.mount(legacy)  # type: ignore[arg-type]
    -
    -    _ = await root.mount(parent, name="parent")
    -
    -
    -@pytest.mark.asyncio
    -async def test_required_dependency_follows_provider_lifecycle() -> None:
    -    events: list[str] = []
    -
    -    class Consumer:
    -        name = "consumer"
    -        inject = (GREETING,)
    -
    -        async def apply(self, ctx) -> None:
    -            events.append(f"load:{ctx.require(GREETING)}")
    -            await ctx.effect(
    -                lambda: lambda: events.append("unload"),
    -                label="consumer",
    -            )
    -
    -    root = CompositionRoot("required-lifecycle")
    -    consumer_plugin = Consumer()
    -    consumer = await root.mount(
    -        consumer_plugin.apply,
    -        name=consumer_plugin.name,
    -        inject=consumer_plugin.inject,
    -    )
    -    assert consumer.state == FiberState.PENDING
    -    assert root.receipt().required_pending == ("consumer",)
    -
    -    first_plugin = GreetingProvider("first")
    -    first_provider = await root.mount(first_plugin.apply, name=first_plugin.name)
    -    assert consumer.state == FiberState.ACTIVE
    -    assert events == ["load:first"]
    -    assert root.receipt().ready is True
    -
    -    await first_provider.dispose()
    -    assert consumer.state == FiberState.PENDING
    -    assert events == ["load:first", "unload"]
    -
    -    second_plugin = GreetingProvider("second")
    -    await root.mount(second_plugin.apply, name=second_plugin.name)
    -    assert consumer.state == FiberState.ACTIVE
    -    assert events == ["load:first", "unload", "load:second"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_nested_inject_is_optional_for_candidate_readiness() -> None:
    -    events: list[str] = []
    -
    -    class Parent:
    -        name = "parent"
    -        inject = ()
    -
    -        async def apply(self, ctx) -> None:
    -            async def use_formatter(inner) -> None:
    -                formatter = inner.require(FORMATTER)
    -                events.append(formatter("ready"))
    -
    -            await ctx.inject((FORMATTER,), use_formatter, name="optional-formatter")
    -
    -    root = CompositionRoot("optional-inject")
    -    parent_plugin = Parent()
    -    parent = await root.mount(parent_plugin.apply, name=parent_plugin.name)
    -    receipt = root.receipt()
    -    assert parent.state == FiberState.ACTIVE
    -    assert receipt.ready is True
    -    assert receipt.optional_pending == ("optional-formatter",)
    -
    -    class FormatterProvider:
    -        name = "formatter-provider"
    -        inject = ()
    -
    -        async def apply(self, ctx) -> None:
    -            await ctx.provide(FORMATTER, lambda value: value.upper())
    -
    -    formatter_plugin = FormatterProvider()
    -    await root.mount(formatter_plugin.apply, name=formatter_plugin.name)
    -    assert events == ["READY"]
    -    assert root.receipt().optional_pending == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_effect_cleanup_is_lifo_and_public_dispose_is_single_shot() -> None:
    -    events: list[str] = []
    -    root = CompositionRoot("effect-lifo")
    -
    -    async def apply(ctx) -> None:
    -        effect = await ctx.effect(
    -            lambda: (
    -                lambda: events.append("first"),
    -                lambda: events.append("second"),
    -            ),
    -            label="pair",
    -        )
    -        await effect.aclose()
    -        await effect.aclose()
    -
    -    fiber = await root.mount(apply, name="effect-owner")
    -    assert fiber.state == FiberState.ACTIVE
    -    assert events == ["second", "first"]
    -    assert fiber.effects == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_effect_setup_failure_rolls_back_collected_cleanup() -> None:
    -    cleanups: list[str] = []
    -    root = CompositionRoot("effect-rollback")
    -
    -    def broken_setup():
    -        yield lambda: cleanups.append("rolled-back")
    -        raise RuntimeError("setup failed")
    -
    -    async def apply(ctx) -> None:
    -        await ctx.effect(broken_setup, label="broken")
    -
    -    fiber = await root.mount(apply, name="broken-plugin")
    -    assert fiber.state == FiberState.FAILED
    -    assert cleanups == ["rolled-back"]
    -    assert fiber.effects == []
    -    assert root.receipt().ready is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_reentrant_dispose_awaits_setup_and_async_cleanup() -> None:
    -    setup_gate = asyncio.Event()
    -    cleanup_gate = asyncio.Event()
    -    cleanup_started = asyncio.Event()
    -    dispose_created = asyncio.Event()
    -    dispose_task: asyncio.Task[None] | None = None
    -    root = CompositionRoot("reentrant-dispose")
    -
    -    async def apply(ctx) -> None:
    -        async def setup():
    -            nonlocal dispose_task
    -            dispose_task = asyncio.create_task(ctx.fiber.dispose())
    -            dispose_created.set()
    -            await setup_gate.wait()
    -
    -            async def cleanup() -> None:
    -                cleanup_started.set()
    -                await cleanup_gate.wait()
    -
    -            return cleanup
    -
    -        await ctx.effect(setup, label="reentrant")
    -
    -    mount_task = asyncio.create_task(root.mount(apply, name="owner"))
    -    await dispose_created.wait()
    -    assert dispose_task is not None
    -    assert dispose_task.done() is False
    -    setup_gate.set()
    -    await cleanup_started.wait()
    -    assert dispose_task.done() is False
    -    cleanup_gate.set()
    -    await dispose_task
    -    fiber = await mount_task
    -    assert fiber.state == FiberState.DISPOSED
    -    assert fiber.effects == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_reentrant_restart_awaits_setup_cleanup_and_reloads() -> None:
    -    setup_gate = asyncio.Event()
    -    cleanup_gate = asyncio.Event()
    -    cleanup_started = asyncio.Event()
    -    restart_created = asyncio.Event()
    -    restart_task: asyncio.Task[None] | None = None
    -    apply_calls = 0
    -    root = CompositionRoot("reentrant-restart")
    -
    -    async def apply(ctx) -> None:
    -        nonlocal apply_calls, restart_task
    -        apply_calls += 1
    -        if apply_calls != 1:
    -            return
    -
    -        async def setup():
    -            nonlocal restart_task
    -            restart_task = asyncio.create_task(ctx.fiber.restart())
    -            restart_created.set()
    -            await setup_gate.wait()
    -
    -            async def cleanup() -> None:
    -                cleanup_started.set()
    -                await cleanup_gate.wait()
    -
    -            return cleanup
    -
    -        _ = await ctx.effect(setup, label="reentrant")
    -
    -    mount_task = asyncio.create_task(root.mount(apply, name="owner"))
    -    await restart_created.wait()
    -    assert restart_task is not None
    -    setup_gate.set()
    -    await cleanup_started.wait()
    -    assert restart_task.done() is False
    -    cleanup_gate.set()
    -    await restart_task
    -    fiber = await mount_task
    -    assert fiber.state == FiberState.ACTIVE
    -    assert fiber.effects == []
    -    assert apply_calls == 2
    -
    -
    -@pytest.mark.asyncio
    -async def test_direct_reentrant_lifecycle_wait_fails_loud_instead_of_deadlock() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("direct-reentrant-wait")
    -
    -    async def apply(ctx) -> None:
    -        for operation in (ctx.fiber.dispose, ctx.fiber.restart):
    -            with pytest.raises(CompositionError) as caught:
    -                await operation()
    -            observed.append(caught.value.code)
    -
    -    fiber = await asyncio.wait_for(root.mount(apply, name="owner"), timeout=0.5)
    -    assert observed == [
    -        "REENTRANT_LIFECYCLE_WAIT",
    -        "REENTRANT_LIFECYCLE_WAIT",
    -    ]
    -    assert fiber.state == FiberState.ACTIVE
    -
    -
    -@pytest.mark.asyncio
    -async def test_unloading_rejects_new_effect_registration() -> None:
    -    errors: list[CompositionError] = []
    -    root = CompositionRoot("inactive-effect")
    -
    -    async def apply(ctx) -> None:
    -        async def cleanup() -> None:
    -            try:
    -                await ctx.effect(lambda: None, label="too-late")
    -            except CompositionError as error:
    -                errors.append(error)
    -
    -        await ctx.effect(lambda: cleanup, label="owner")
    -
    -    fiber = await root.mount(apply, name="plugin")
    -    await fiber.restart()
    -    assert [error.code for error in errors] == ["INACTIVE_EFFECT"]
    -    assert fiber.state == FiberState.ACTIVE
    -
    -
    -@pytest.mark.asyncio
    -async def test_caller_cancellation_does_not_truncate_disposal() -> None:
    -    cleanup_started = asyncio.Event()
    -    cleanup_gate = asyncio.Event()
    -    cleanup_finished = False
    -    root = CompositionRoot("cancel-safe-cleanup")
    -
    -    async def apply(ctx) -> None:
    -        async def cleanup() -> None:
    -            nonlocal cleanup_finished
    -            cleanup_started.set()
    -            await cleanup_gate.wait()
    -            cleanup_finished = True
    -
    -        await ctx.effect(lambda: cleanup, label="slow-cleanup")
    -
    -    fiber = await root.mount(apply, name="owner")
    -    caller = asyncio.create_task(fiber.dispose())
    -    await cleanup_started.wait()
    -    caller.cancel()
    -    await asyncio.sleep(0)
    -    assert caller.done() is False
    -    cleanup_gate.set()
    -    with pytest.raises(asyncio.CancelledError):
    -        await caller
    -    assert cleanup_finished is True
    -    assert fiber.state == FiberState.DISPOSED
    -    assert root.receipt().effects == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mount_cancellation_rolls_back_published_fiber() -> None:
    -    apply_started = asyncio.Event()
    -    cleanup_finished = False
    -    root = CompositionRoot("cancelled-mount")
    -
    -    async def apply(ctx) -> None:
    -        nonlocal cleanup_finished
    -        await ctx.effect(
    -            lambda: lambda: _mark_cleanup(),
    -            label="mount-cleanup",
    -        )
    -        apply_started.set()
    -        await asyncio.Event().wait()
    -
    -    def _mark_cleanup() -> None:
    -        nonlocal cleanup_finished
    -        cleanup_finished = True
    -
    -    mount_task = asyncio.create_task(root.mount(apply, name="cancelled"))
    -    await apply_started.wait()
    -    mount_task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await mount_task
    -    assert cleanup_finished is True
    -    assert root.receipt().fibers == ()
    -    assert root.root_fiber.children == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_stale_dependency_epoch_never_becomes_active() -> None:
    -    apply_started = asyncio.Event()
    -    apply_gate = asyncio.Event()
    -    events: list[str] = []
    -    root = CompositionRoot("stale-epoch")
    -    provider = await _mount_greeting(root)
    -
    -    async def consume(ctx) -> None:
    -        events.append(f"start:{ctx.require(GREETING)}")
    -        apply_started.set()
    -        await apply_gate.wait()
    -        await ctx.effect(lambda: lambda: events.append("cleanup"), label="owned")
    -
    -    mount_task = asyncio.create_task(
    -        root.mount(consume, name="slow-consumer", inject=(GREETING,))
    -    )
    -    await apply_started.wait()
    -    remove_task = asyncio.create_task(provider.dispose())
    -    apply_gate.set()
    -    consumer = await mount_task
    -    await remove_task
    -    assert consumer.state == FiberState.PENDING
    -    assert events == ["start:hello", "cleanup"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_mount_observer_failure_rolls_back_parent_ownership() -> None:
    -    root = CompositionRoot("publication-rollback")
    -
    -    def fail_publication(fiber) -> None:
    -        if fiber.name == "broken-publication":
    -            raise RuntimeError("publication failed")
    -
    -    root.on_mount(fail_publication)
    -    with pytest.raises(RuntimeError, match="publication failed"):
    -        await root.mount(lambda _: None, name="broken-publication")
    -    assert root.root_fiber.children == []
    -    assert root.receipt().fibers == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_parent_disposal_during_publication_drains_pending_child() -> None:
    -    root = CompositionRoot("parent-child-quiescence")
    -    owner_context = None
    -    cleanup_started = asyncio.Event()
    -    cleanup_gate = asyncio.Event()
    -    child_apply_calls = 0
    -    parent_dispose_task: asyncio.Task[None] | None = None
    -
    -    async def owner_apply(ctx) -> None:
    -        nonlocal owner_context
    -        owner_context = ctx
    -
    -    owner = await root.mount(owner_apply, name="owner")
    -
    -    async def observe_child(fiber) -> None:
    -        nonlocal parent_dispose_task
    -        if fiber.name != "child":
    -            return
    -
    -        async def cleanup() -> None:
    -            cleanup_started.set()
    -            await cleanup_gate.wait()
    -
    -        _ = await fiber.context.effect(lambda: cleanup, label="pending-child")
    -        parent_dispose_task = asyncio.create_task(owner.dispose())
    -        await cleanup_started.wait()
    -
    -    root.on_mount(observe_child)
    -
    -    async def child_apply(_) -> None:
    -        nonlocal child_apply_calls
    -        child_apply_calls += 1
    -
    -    assert owner_context is not None
    -    child_mount = asyncio.create_task(owner_context.mount(child_apply, name="child"))
    -    await cleanup_started.wait()
    -    assert child_apply_calls == 0
    -    assert parent_dispose_task is not None
    -    assert parent_dispose_task.done() is False
    -    cleanup_gate.set()
    -    child = await child_mount
    -    await parent_dispose_task
    -    assert child.state == FiberState.DISPOSED
    -    assert owner.state == FiberState.DISPOSED
    -
    -
    -@pytest.mark.asyncio
    -async def test_dispose_observer_failure_is_contained_and_peers_run() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("observer-containment")
    -
    -    def broken(fiber) -> None:
    -        if fiber.name == "child":
    -            raise RuntimeError("broken observer")
    -
    -    root.on_dispose(broken)
    -    root.on_dispose(lambda fiber: observed.append(fiber.name))
    -    child = await root.mount(lambda _: None, name="child")
    -    await child.dispose()
    -    assert observed == ["child"]
    -    assert any(
    -        "broken observer" in incident.message for incident in root.receipt().incidents
    -    )
    -    assert root.receipt().ready is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_duplicate_provider_fails_without_replacing_first_owner() -> None:
    -    root = CompositionRoot("duplicate-service")
    -    first = await _mount_greeting(root, "first")
    -    duplicate = await _mount_greeting(root, "second", name="second-provider")
    -    assert first.state == FiberState.ACTIVE
    -    assert duplicate.state == FiberState.FAILED
    -    assert root.context.require(GREETING) == "first"
    -    assert root.receipt().services == ("greeting",)
    -
    -
    -@pytest.mark.asyncio
    -async def test_root_disposal_drains_children_effects_and_services() -> None:
    -    root = CompositionRoot("root-dispose")
    -    await _mount_greeting(root)
    -    await root.dispose()
    -    receipt = root.receipt()
    -    assert receipt.fibers == ()
    -    assert receipt.services == ()
    -    assert receipt.effects == ()
    -    assert receipt.ready is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_root_disposal_rejects_mount_during_slow_cleanup() -> None:
    -    cleanup_started = asyncio.Event()
    -    cleanup_gate = asyncio.Event()
    -    root = CompositionRoot("root-dispose-mount-race")
    -
    -    async def cleanup() -> None:
    -        cleanup_started.set()
    -        await cleanup_gate.wait()
    -
    -    _ = await root.context.effect(lambda: cleanup, label="slow-root-cleanup")
    -    dispose_task = asyncio.create_task(root.dispose())
    -    await cleanup_started.wait()
    -    with pytest.raises(CompositionError) as caught:
    -        await root.mount(lambda _: None, name="too-late")
    -    assert caught.value.code == "INACTIVE_PLUGIN_OWNER"
    -    cleanup_gate.set()
    -    await dispose_task
    -    assert root.receipt().fibers == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_provider_cleanup_survives_all_dependent_cleanup_failures() -> None:
    -    cleaned: list[str] = []
    -    root = CompositionRoot("dependent-cleanup-failure")
    -    provider = await _mount_greeting(root)
    -
    -    async def consume(ctx) -> None:
    -        name = ctx.fiber.name
    -
    -        def fail_cleanup() -> None:
    -            cleaned.append(name)
    -            raise RuntimeError(f"cleanup failed: {name}")
    -
    -        _ = await ctx.effect(lambda: fail_cleanup, label=f"cleanup:{name}")
    -
    -    _ = await root.mount(consume, name="consumer-a", inject=(GREETING,))
    -    _ = await root.mount(consume, name="consumer-b", inject=(GREETING,))
    -    with pytest.raises(BaseExceptionGroup, match="Fiber 卸载失败"):
    -        await provider.dispose()
    -
    -    assert sorted(cleaned) == ["consumer-a", "consumer-b"]
    -    assert provider.state == FiberState.DISPOSED
    -    assert root.context.get(GREETING) is None
    -    replacement = await _mount_greeting(root, name="replacement-provider")
    -    assert replacement.state == FiberState.ACTIVE
    -    assert root.context.require(GREETING) == "hello"
    -
    -
    -@pytest.mark.asyncio
    -async def test_observer_cancelled_error_is_contained_when_owner_not_cancelled() -> None:
    -    observed: list[str] = []
    -    root = CompositionRoot("observer-cancelled-error")
    -
    -    async def cancelled(_fiber) -> None:
    -        raise asyncio.CancelledError("observer cancelled itself")
    -
    -    root.on_dispose(cancelled)
    -    root.on_dispose(lambda fiber: observed.append(fiber.name))
    -    child = await root.mount(lambda _: None, name="child")
    -    await child.dispose()
    -    assert observed == ["child"]
    -    assert any(
    -        incident.error_type == "CancelledError" for incident in root.receipt().incidents
    -    )
    -    assert root.receipt().ready is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_explicit_health_and_incident_are_independent_facts() -> None:
    -    root = CompositionRoot("health-incident")
    -    handles: list[HealthHandle] = []
    -
    -    async def plugin(ctx) -> None:
    -        handles.append(await ctx.health("poller", required=True))
    -        _ = ctx.report_incident("poll_failed", "first attempt failed")
    -
    -    await root.mount(plugin, name="watcher")
    -    initial = root.receipt()
    -    assert initial.ready is True
    -    assert initial.required_degraded == ()
    -    assert initial.incident_sequence == 1
    -
    -    handles[0].degrade("upstream unavailable")
    -    degraded = root.receipt()
    -    assert degraded.ready is False
    -    assert degraded.required_degraded == ("watcher:poller",)
    -    assert degraded.incident_sequence == 1
    -
    -    handles[0].recover()
    -    recovered = root.receipt()
    -    assert recovered.ready is True
    -    assert recovered.required_degraded == ()
    -    assert recovered.incidents == initial.incidents
    -    assert "watcher:health:poller" in recovered.effects
    -    _ = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    await root.dispose()
    -    with pytest.raises(CompositionError) as caught:
    -        handles[0].recover()
    -    assert caught.value.code == "INACTIVE_HEALTH"
    -
    -
    -@pytest.mark.asyncio
    -async def test_fiber_restart_invalidates_old_health_handle() -> None:
    -    root = CompositionRoot("health-restart")
    -    handles: list[HealthHandle] = []
    -
    -    async def plugin(ctx) -> None:
    -        handles.append(await ctx.health("worker", required=True))
    -
    -    fiber = await root.mount(plugin, name="plugin")
    -    first = handles[0]
    -    first.degrade("first epoch failed")
    -
    -    await fiber.restart()
    -
    -    assert len(handles) == 2
    -    assert root.receipt().required_degraded == ()
    -    with pytest.raises(CompositionError) as caught:
    -        first.recover()
    -    assert caught.value.code == "INACTIVE_HEALTH"
    -    handles[1].degrade("second epoch failed")
    -    assert root.receipt().required_degraded == ("plugin:worker",)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_optional_health_degradation_does_not_block_readiness() -> None:
    -    root = CompositionRoot("optional-health")
    -    handles: list[HealthHandle] = []
    -
    -    async def plugin(ctx) -> None:
    -        handles.append(await ctx.health("telemetry", required=False))
    -
    -    await root.mount(plugin, name="observer")
    -    handles[0].degrade("metrics endpoint unavailable")
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is True
    -    assert receipt.required_degraded == ()
    -    assert receipt.health[0].healthy is False
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_optional_fiber_failure_records_incident_without_poisoning_root() -> None:
    -    root = CompositionRoot("optional-failure")
    -
    -    async def broken(_) -> None:
    -        raise RuntimeError("optional failed")
    -
    -    _ = await root.context.inject((), broken, name="optional")
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is True
    -    assert receipt.optional_pending == ("optional",)
    -    assert any(incident.message == "optional failed" for incident in receipt.incidents)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_incident_overflow_fails_loud() -> None:
    -    root = CompositionRoot("candidate-incidents", candidate_incident_limit=2)
    -    _ = await root.mount(lambda _: None, name="plugin")
    -
    -    for index in range(3):
    -        _ = root.context.report_incident("probe", f"failure {index}")
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is False
    -    assert receipt.incident_sequence == 3
    -    assert receipt.incident_overflowed is True
    -    assert tuple(item.sequence for item in receipt.incidents) == (1, 2)
    -    with pytest.raises(RuntimeError, match="incident_overflowed=True"):
    -        RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_stable_incident_buffer_is_bounded_without_poisoning_health() -> None:
    -    root = CompositionRoot("stable-incidents")
    -    _ = await root.mount(lambda _: None, name="plugin")
    -
    -    for index in range(root.RECENT_INCIDENT_LIMIT + 2):
    -        _ = root.context.report_incident("probe", f"failure {index}")
    -
    -    receipt = root.receipt()
    -    assert receipt.ready is True
    -    assert receipt.incident_overflowed is False
    -    assert receipt.incident_sequence == root.RECENT_INCIDENT_LIMIT + 2
    -    assert len(receipt.incidents) == root.RECENT_INCIDENT_LIMIT
    -    assert receipt.incidents[0].sequence == 3
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_incident_after_candidate_seal_invalidates_validation_receipt() -> None:
    -    root = CompositionRoot("sealed-incidents", candidate_incident_limit=8)
    -    _ = await root.mount(lambda _: None, name="plugin")
    -    compiler = RuntimeSnapshotCompiler()
    -    candidate = compiler.compile({}, composition_root=root)
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    transaction = store.begin_publish(candidate)
    -    await store.commit_latest(transaction)
    -    store.pause_candidate_admission(candidate)
    -    store.seal_candidate_validation(candidate)
    -
    -    _ = root.context.report_incident("late_failure", "after seal")
    -
    -    with pytest.raises(RuntimeError, match="验证回执在封存后发生变化"):
    -        await store.promote_latest()
    -    _ = await store.discard_latest(candidate)
    -    await store.close()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_rebuilt_formal_root_health_is_rechecked_after_candidate_seal() -> None:
    -    compiler = RuntimeSnapshotCompiler()
    -    validation_root = CompositionRoot("validation-root")
    -    _ = await validation_root.mount(lambda _: None, name="plugin")
    -    candidate = compiler.compile({}, composition_root=validation_root)
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    await store.commit_latest(store.begin_publish(candidate))
    -    store.pause_candidate_admission(candidate)
    -    store.seal_candidate_validation(candidate)
    -
    -    formal_root = CompositionRoot("formal-root")
    -    handles: list[HealthHandle] = []
    -
    -    async def plugin(ctx) -> None:
    -        handles.append(await ctx.health("worker", required=True))
    -
    -    _ = await formal_root.mount(plugin, name="plugin")
    -    formal_snapshot = compiler.compile({}, composition_root=formal_root)
    -    candidate.composition_root = formal_root
    -    candidate.composition_topology = formal_snapshot.composition_topology
    -    handles[0].degrade("formal worker unavailable")
    -
    -    with pytest.raises(RuntimeError, match="required_degraded"):
    -        await store.promote_latest()
    -
    -    _ = await store.discard_latest(candidate)
    -    await store.close()
    -    await validation_root.dispose()
    -    await formal_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_reused_stable_root_must_still_be_ready() -> None:
    -    compiler = RuntimeSnapshotCompiler()
    -    validation_root = CompositionRoot("validation-root")
    -    _ = await validation_root.mount(lambda _: None, name="plugin")
    -    candidate = compiler.compile({}, composition_root=validation_root)
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    await store.commit_latest(store.begin_publish(candidate))
    -    store.pause_candidate_admission(candidate)
    -    store.seal_candidate_validation(candidate)
    -
    -    reused_stable_root = CompositionRoot("reused-stable")
    -    _ = await reused_stable_root.mount(
    -        lambda _: None,
    -        name="missing-consumer",
    -        inject=(GREETING,),
    -    )
    -    reused_snapshot = compiler.compile(
    -        {},
    -        composition_root=reused_stable_root,
    -        require_composition_ready=False,
    -    )
    -    candidate.composition_root = reused_stable_root
    -    candidate.composition_topology = reused_snapshot.composition_topology
    -
    -    with pytest.raises(RuntimeError, match="required_pending"):
    -        await store.promote_latest()
    -
    -    _ = await store.discard_latest(candidate)
    -    await store.close()
    -    await validation_root.dispose()
    -    await reused_stable_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_compiler_rejects_unready_required_topology() -> None:
    -    root = CompositionRoot("candidate-unready")
    -    await root.mount(
    -        lambda _: None,
    -        name="missing-consumer",
    -        inject=(GREETING,),
    -    )
    -    with pytest.raises(RuntimeError, match="required_pending"):
    -        RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_store_publishes_complete_composition_root() -> None:
    -    drained: list[str] = []
    -
    -    async def dispose_snapshot(snapshot) -> None:
    -        if snapshot.composition_root is not None:
    -            drained.append(snapshot.composition_root.generation_id)
    -            await snapshot.composition_root.dispose()
    -
    -    compiler = RuntimeSnapshotCompiler()
    -    stable = compiler.compile({}, snapshot_revision="stable")
    -    root = CompositionRoot("candidate-ready")
    -    await _mount_greeting(root)
    -    candidate = compiler.compile(
    -        {},
    -        snapshot_revision="candidate",
    -        composition_root=root,
    -    )
    -    store = RuntimeSnapshotStore(dispose_snapshot)
    -    store.install(stable)
    -    transaction = store.begin_publish(candidate)
    -    await store.commit_latest(transaction)
    -
    -    lease = store.lease(selector="latest")
    -    assert lease.snapshot.composition_root is root
    -    assert lease.snapshot.composition_topology == root.topology_view()
    -    assert lease.snapshot.composition_root.context.require(GREETING) == "hello"
    -    await lease.release()
    -
    -    store.pause_candidate_admission(candidate)
    -    store.seal_candidate_validation(candidate)
    -    await store.promote_latest()
    -    await store.retry_drains()
    -    assert store.current is candidate
    -    assert drained == []
    -    await store.close()
    -    assert drained == ["candidate-ready"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_store_rejects_topology_drift_after_compile() -> None:
    -    root = CompositionRoot("candidate-drift")
    -    provider = await _mount_greeting(root)
    -    await root.mount(
    -        lambda _: None,
    -        name="consumer",
    -        inject=(GREETING,),
    -    )
    -    candidate = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    compiled_view = candidate.composition_topology
    -    await provider.dispose()
    -
    -    assert compiled_view is not None
    -    assert candidate.composition_topology is compiled_view
    -    assert compiled_view.identity != root.topology_identity()
    -
    -    store = RuntimeSnapshotStore()
    -    store.install(RuntimeSnapshotCompiler().compile({}))
    -    with pytest.raises(RuntimeError, match="组合拓扑未就绪"):
    -        store.begin_publish(candidate)
    -
    -
    -@pytest.mark.asyncio
    -async def test_topology_identity_excludes_mutable_state_and_generic_effects() -> None:
    -    root = CompositionRoot("immutable-topology")
    -    fiber = await root.mount(lambda _: None, name="optional")
    -    compiled = root.topology_view()
    -    compiled_snapshot = RuntimeSnapshotCompiler().compile(
    -        {},
    -        snapshot_revision="immutable-topology",
    -        composition_root=root,
    -    )
    -
    -    fiber.state = FiberState.PENDING
    -    effect = await root.context.effect(lambda: None, label="diagnostic")
    -    observed = root.topology_view()
    -
    -    assert observed.identity == compiled.identity
    -    assert observed.composition_revision == compiled.composition_revision
    -    assert observed.effects != compiled.effects
    -
    -    await effect.aclose()
    -    fiber.state = FiberState.ACTIVE
    -    assert root.topology_identity() == compiled.identity
    -    assert root.composition_revision == compiled.composition_revision
    -    rebuilt_snapshot = RuntimeSnapshotCompiler().compile(
    -        {},
    -        snapshot_revision="immutable-topology",
    -        composition_root=root,
    -    )
    -    assert rebuilt_snapshot.snapshot_id == compiled_snapshot.snapshot_id
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_fiber_handle_hides_core_owned_mutable_state() -> None:
    -    root = CompositionRoot("fiber-handle")
    -    handles: list[object] = []
    -
    -    async def apply(ctx) -> None:
    -        handles.append(ctx.fiber)
    -        handles.append(await ctx.mount(lambda _: None, name="child"))
    -
    -    await root.mount(apply, name="owner")
    -
    -    for handle in handles:
    -        assert not hasattr(handle, "effects")
    -        assert not hasattr(handle, "children")
    -        assert not hasattr(handle, "dependencies")
    -        with pytest.raises(AttributeError):
    -            setattr(handle, "state", FiberState.DISPOSED)
    -
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_provider_restart_alone_invalidates_sealed_revision() -> None:
    -    root = CompositionRoot("service-revision")
    -    provider = await _mount_greeting(root)
    -    compiler = RuntimeSnapshotCompiler()
    -    candidate = compiler.compile({}, composition_root=root)
    -    compiled = candidate.composition_topology
    -    assert compiled is not None
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    transaction = store.begin_publish(candidate)
    -    await store.commit_latest(transaction)
    -
    -    await provider.restart()
    -
    -    restored = root.topology_view()
    -    assert restored.identity == compiled.identity
    -    assert restored.composition_revision == compiled.composition_revision + 2
    -    store.pause_candidate_admission(candidate)
    -    with pytest.raises(RuntimeError, match="发生过结构变化"):
    -        store.seal_candidate_validation(candidate)
    -    _ = await store.discard_latest(candidate)
    -    await store.close()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_publication_participant_can_retain_closed_exact_target() -> None:
    -    compiler = RuntimeSnapshotCompiler()
    -    store = RuntimeSnapshotStore()
    -    stable = compiler.compile({}, snapshot_revision="stable")
    -    candidate = compiler.compile({}, snapshot_revision="candidate")
    -    store.install(stable)
    -    transaction = store.begin_publish(candidate)
    -
    -    with pytest.raises(RuntimeError, match="不可租用"):
    -        store.lease(candidate.snapshot_id)
    -    retained = store.retain_publication_target(transaction)
    -
    -    assert retained.snapshot is candidate
    -    assert retained.active
    -    assert candidate.lease_count == 1
    -    await retained.release()
    -    await store.abort(transaction)
    -    with pytest.raises(RuntimeError, match="target 已失效"):
    -        store.retain_publication_target(transaction)
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_fiber_replace_alone_invalidates_sealed_revision() -> None:
    -    root = CompositionRoot("fiber-revision")
    -    fiber = await root.mount(lambda _: None, name="plain")
    -    compiler = RuntimeSnapshotCompiler()
    -    candidate = compiler.compile({}, composition_root=root)
    -    compiled = candidate.composition_topology
    -    assert compiled is not None
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    transaction = store.begin_publish(candidate)
    -    await store.commit_latest(transaction)
    -
    -    await fiber.dispose()
    -    _ = await root.mount(lambda _: None, name="plain")
    -
    -    restored = root.topology_view()
    -    assert restored.identity == compiled.identity
    -    assert restored.composition_revision == compiled.composition_revision + 2
    -    store.pause_candidate_admission(candidate)
    -    with pytest.raises(RuntimeError, match="发生过结构变化"):
    -        store.seal_candidate_validation(candidate)
    -    _ = await store.discard_latest(candidate)
    -    await store.close()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_isomorphic_roots_ignore_generation_identity() -> None:
    -    async def build(generation_id: str) -> CompositionRoot:
    -        root = CompositionRoot(generation_id)
    -        _ = await root.mount(lambda _: None, name="same-plugin")
    -        return root
    -
    -    first = await build("candidate-generation")
    -    second = await build("production-generation")
    -    compiler = RuntimeSnapshotCompiler()
    -    first_snapshot = compiler.compile(
    -        {},
    -        snapshot_revision="same-input",
    -        composition_root=first,
    -    )
    -    second_snapshot = compiler.compile(
    -        {},
    -        snapshot_revision="same-input",
    -        composition_root=second,
    -    )
    -
    -    assert first.topology_identity() == second.topology_identity()
    -    assert first_snapshot.snapshot_id == second_snapshot.snapshot_id
    -    await first.dispose()
    -    await second.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_topology_identity_includes_parent_ownership() -> None:
    -    async def build(*, nested: bool) -> CompositionRoot:
    -        root = CompositionRoot("nested" if nested else "flat")
    -
    -        async def apply_host(ctx) -> None:
    -            async def apply_group(group_ctx) -> None:
    -                if nested:
    -                    _ = await group_ctx.mount(lambda _: None, name="worker")
    -
    -            _ = await ctx.mount(apply_group, name="group")
    -            if not nested:
    -                _ = await ctx.mount(lambda _: None, name="worker")
    -
    -        _ = await root.mount(apply_host, name="host")
    -        return root
    -
    -    nested = await build(nested=True)
    -    flat = await build(nested=False)
    -    nested_view = nested.topology_view()
    -    flat_view = flat.topology_view()
    -    compiler = RuntimeSnapshotCompiler()
    -
    -    assert nested_view.composition_revision == flat_view.composition_revision
    -    assert tuple((item.name, item.parent) for item in nested_view.fibers) == (
    -        ("group", "host"),
    -        ("host", None),
    -        ("worker", "group"),
    -    )
    -    assert tuple((item.name, item.parent) for item in flat_view.fibers) == (
    -        ("group", "host"),
    -        ("host", None),
    -        ("worker", "host"),
    -    )
    -    assert nested_view.identity != flat_view.identity
    -    assert (
    -        compiler.compile(
    -            {},
    -            snapshot_revision="same-input",
    -            composition_root=nested,
    -        ).snapshot_id
    -        != compiler.compile(
    -            {},
    -            snapshot_revision="same-input",
    -            composition_root=flat,
    -        ).snapshot_id
    -    )
    -
    -    await nested.dispose()
    -    await flat.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_topology_view_identity_includes_declared_dependencies() -> None:
    -    async def build(dependency: ServiceKey[object]) -> str:
    -        root = CompositionRoot("dependency-view")
    -        greeting_plugin = GreetingProvider()
    -        await root.mount(greeting_plugin.apply, name=greeting_plugin.name)
    -
    -        class FormatterProvider:
    -            name = "formatter-provider"
    -            inject = ()
    -
    -            async def apply(self, ctx) -> None:
    -                await ctx.provide(FORMATTER, lambda value: value)
    -
    -        formatter_plugin = FormatterProvider()
    -        await root.mount(formatter_plugin.apply, name=formatter_plugin.name)
    -        await root.mount(
    -            lambda _: None,
    -            name="consumer",
    -            inject=(dependency,),
    -        )
    -        return root.topology_identity()
    -
    -    greeting = await build(GREETING)
    -    formatter = await build(FORMATTER)
    -
    -    assert greeting != formatter
    -
    -
    -@pytest.mark.asyncio
    -async def test_promotion_rechecks_candidate_topology_after_behavior_probe() -> None:
    -    root = CompositionRoot("candidate-promotion-recheck")
    -    provider = await _mount_greeting(root)
    -    await root.mount(
    -        lambda _: None,
    -        name="consumer",
    -        inject=(GREETING,),
    -    )
    -    compiler = RuntimeSnapshotCompiler()
    -    candidate = compiler.compile({}, composition_root=root)
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    transaction = store.begin_publish(candidate)
    -    await store.commit_latest(transaction)
    -
    -    await provider.dispose()
    -    store.pause_candidate_admission(candidate)
    -    with pytest.raises(RuntimeError, match="组合拓扑未就绪"):
    -        await store.promote_latest()
    -
    -    _ = await _mount_greeting(root)
    -    assert root.topology_identity() == candidate.composition_topology.identity  # type: ignore[union-attr]
    -    with pytest.raises(RuntimeError, match="发生过结构变化"):
    -        store.seal_candidate_validation(candidate)
    -
    -    _ = await store.discard_latest(candidate)
    -    rebuilt = compiler.compile({}, composition_root=root)
    -    transaction = store.begin_publish(rebuilt)
    -    await store.commit_latest(transaction)
    -    store.pause_candidate_admission(rebuilt)
    -    store.seal_candidate_validation(rebuilt)
    -    _ = await store.promote_latest()
    -    assert store.current is rebuilt
    -    await store.close()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_manager_drains_snapshot_composition_root() -> None:
    -    cleaned: list[str] = []
    -    root = CompositionRoot("manager-drain")
    -
    -    async def apply(ctx) -> None:
    -        await ctx.effect(
    -            lambda: lambda: cleaned.append("cleaned"),
    -            label="owned",
    -        )
    -
    -    await root.mount(apply, name="plugin")
    -    snapshot = RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -    manager = object.__new__(PluginManager)
    -    manager._snapshot_store = RuntimeSnapshotStore()
    -    manager._snapshot_skill_catalogs = {}
    -    manager._dashboard_validation_releaser = None
    -    manager._finish_drained_reload = lambda _: None
    -    manager._runtime_started_roots = set()
    -    manager._runtime_lifecycle_lock = asyncio.Lock()
    -
    -    await manager._on_snapshot_drained(snapshot)
    -    assert cleaned == ["cleaned"]
    -    assert root.receipt().ready is False
    -
    -
    -def test_core_plugin_data_access_records_scoped_writes(tmp_path) -> None:
    -    audit = CompositionAudit()
    -    access = PluginDataAccess(tmp_path, audit)
    -    data = access.for_plugin("probe")
    -    target = data.write_text("state/value.json", '{"value": 1}\n')
    -    assert target.relative_to(tmp_path).as_posix() == (
    -        "plugin-data/probe/state/value.json"
    -    )
    -    assert data.read_text("state/value.json") == '{"value": 1}\n'
    -    assert [
    -        (write.plugin_id, write.operation, write.relative_path)
    -        for write in audit.writes
    -    ] == [("probe", "create", "state/value.json")]
    -    with pytest.raises(ValueError, match="相对路径无效"):
    -        data.write_text("../escape", "blocked")
    -
    -
    -def test_core_plugin_data_access_never_follows_scoped_symlinks(tmp_path) -> None:
    -    audit = CompositionAudit()
    -    data = PluginDataAccess(tmp_path, audit).for_plugin("probe")
    -    outside = tmp_path / "outside"
    -    outside.mkdir()
    -    (data.root / "escape").symlink_to(outside, target_is_directory=True)
    -
    -    with pytest.raises(OSError):
    -        data.write_text("escape/value.json", "blocked")
    -    assert list(outside.iterdir()) == []
    -
    -
    -def test_external_effect_gate_records_denial() -> None:
    -    audit = CompositionAudit()
    -    gate = ExternalEffectGate(audit)
    -    with pytest.raises(PermissionError, match="禁止外部效果"):
    -        gate.authorize(kind="http", target="https://example.invalid")
    -    assert [asdict(effect) for effect in audit.external_effects] == [
    -        {
    -            "kind": "http",
    -            "target": "https://example.invalid",
    -            "outcome": "denied",
    -        }
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_external_effect_attempt_rejects_candidate_even_when_caught() -> None:
    -    audit = CompositionAudit()
    -    root = CompositionRoot("external-effect-gate", audit=audit)
    -    gate = ExternalEffectGate(audit)
    -
    -    async def apply(_) -> None:
    -        with pytest.raises(PermissionError):
    -            gate.authorize(kind="http", target="https://example.invalid")
    -
    -    _ = await root.mount(apply, name="caught-denial")
    -    assert root.receipt().ready is False
    -    with pytest.raises(RuntimeError, match="拓扑未就绪"):
    -        RuntimeSnapshotCompiler().compile({}, composition_root=root)
    -
    -
    -@pytest.mark.asyncio
    -async def test_internal_root_cleanup_is_fail_loud_and_not_in_topology() -> None:
    -    root = CompositionRoot("internal-cleanup")
    -
    -    def fail_cleanup() -> None:
    -        raise RuntimeError("cleanup failed")
    -
    -    root._defer_internal_cleanup(  # pyright: ignore[reportPrivateUsage]
    -        "candidate-module",
    -        fail_cleanup,
    -    )
    -    _ = await root.mount(lambda _: None, name="plugin")
    -
    -    assert "candidate-module" not in root.topology_view().effects
    -    with pytest.raises(BaseExceptionGroup, match="Root Context 清理失败"):
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_promotion_requires_sealed_receipt_and_rejects_later_write(
    -    tmp_path,
    -) -> None:
    -    audit = CompositionAudit()
    -    root = CompositionRoot("sealed-validation", audit=audit)
    -    data = PluginDataAccess(tmp_path, audit).for_plugin("probe")
    -    data.write_text("state.json", "first")
    -    _ = await _mount_greeting(root)
    -    compiler = RuntimeSnapshotCompiler()
    -    candidate = compiler.compile({}, composition_root=root)
    -    store = RuntimeSnapshotStore()
    -    store.install(compiler.compile({}))
    -    await store.commit_latest(store.begin_publish(candidate))
    -    store.pause_candidate_admission(candidate)
    -
    -    with pytest.raises(RuntimeError, match="缺少 Core 验证回执"):
    -        await store.promote_latest()
    -    store.seal_candidate_validation(candidate)
    -    data.write_text("state.json", "second")
    -    with pytest.raises(RuntimeError, match="封存后发生变化"):
    -        await store.promote_latest()
    diff --git a/tests/test_plugin_composition_loader.py b/tests/test_plugin_composition_loader.py
    deleted file mode 100644
    index b24aa8c72..000000000
    --- a/tests/test_plugin_composition_loader.py
    +++ /dev/null
    @@ -1,4154 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import hashlib
    -import json
    -import sqlite3
    -import sys
    -from collections.abc import Mapping
    -from pathlib import Path
    -from types import ModuleType, SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -from pydantic import BaseModel
    -
    -import agent.plugins.manager as plugin_manager_module
    -from agent.plugin_composition import (
    -    BACKGROUND_JOBS,
    -    CHANNELS,
    -    AttachmentKind,
    -    ChannelCapability,
    -    ChannelDefinition,
    -    CompositionOverlay,
    -    CompositionRoot,
    -    CredentialRef,
    -    InboundIdentity,
    -    PluginBackgroundJobs,
    -    PluginChannels,
    -    PluginRuntime,
    -    ProviderClientFactory,
    -    ServiceView,
    -)
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.artifacts import ArtifactPointer, read_pointer, write_pointers
    -from agent.plugins.dashboard_host import DashboardBinding, PluginDashboardHost
    -from agent.plugins.generation import PluginGeneration
    -from agent.plugins.generation_activity_host import ActivityHost
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.manifest import write_plugin_manifest
    -from agent.plugins.skill_links import PluginSkillLinker
    -from agent.plugins.snapshot import (
    -    RuntimeSnapshotCompiler,
    -    RuntimeSnapshotStore,
    -    bind_runtime_snapshot,
    -    reset_runtime_snapshot,
    -)
    -from agent.tools.message_push import MessagePushTool
    -from bootstrap.tools import _dispatch_v3_channel_push
    -from bus.event_bus import EventBus
    -from bus.queue import MessageBus
    -from infra.channels.artifacts import ChannelAttachmentArtifactStore
    -from session.store import SessionStore
    -
    -
    -def _write_plugin(root: Path, name: str, source: str) -> Path:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(source, encoding="utf-8")
    -    return plugin_dir
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -def _active_channel_generation(manager: PluginManager):
    -    snapshot = manager.current_snapshot
    -    return (
    -        None
    -        if snapshot is None
    -        else manager.channel_generation_host.get(snapshot.snapshot_id)
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_plugin_without_static_manifest_fails_before_import(
    -    tmp_path: Path,
    -) -> None:
    -    """在任何插件代码或正式数据写入前拒绝无 manifest 的 installed artifact。"""
    -
    -    # 1. 构造缺少静态 admission manifest 的旧 installed artifact。
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "missing_manifest"
    -    plugin_dir = plugin_base / ".artifacts" / "1.0.0-test"
    -    plugin_dir.mkdir(parents=True)
    -    import_marker = plugin_dir / "imported"
    -    (plugin_dir / "plugin.py").write_text(
    -        "from pathlib import Path\n"
    -        f"Path({str(import_marker)!r}).write_text('imported')\n"
    -        "api_version = 3\n"
    -        "name = 'missing_manifest'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_base / ".pointers.json").write_text(
    -        '{"stable":".artifacts/1.0.0-test",' '"latest":".artifacts/1.0.0-test"}\n',
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    # 2. Admission 必须在 import、generation 和正式 data root 之前失败。
    -    with pytest.raises(ValueError, match="缺少静态 manifest"):
    -        await manager.load_all()
    -    assert not import_marker.exists()
    -    assert manager.current_snapshot is None
    -    assert manager.generation("missing_manifest@lab") is None
    -    assert not (
    -        tmp_path / "workspace" / "plugin-data" / "missing_manifest-lab"
    -    ).exists()
    -
    -
    -@pytest.mark.asyncio
    -async def test_replace_snapshot_payload_rebinds_all_exact_root_activity_catalogs() -> (
    -    None
    -):
    -    """让全部 activity catalog 随载荷一起切换到正式 Root。"""
    -
    -    async def compile_snapshot(label: str):
    -        # 1. 每棵 Root 独立拥有 background-job activity catalog。
    -        root = CompositionRoot(label)
    -        _ = await root.context.provide(
    -            BACKGROUND_JOBS,
    -            PluginBackgroundJobs(root.instance_token),
    -        )
    -        snapshot = RuntimeSnapshotCompiler().compile(
    -            {},
    -            composition_root=root,
    -        )
    -        return root, snapshot
    -
    -    validation_root, target = await compile_snapshot("activity:validation")
    -    formal_root, source = await compile_snapshot("activity:formal")
    -    try:
    -        # 2. identity 值应等价,但对象保持可区分,确保六个字段都真实替换。
    -        identity_fields = ("background_job_catalog_identity",)
    -        for name in identity_fields:
    -            target_identity = getattr(target, name)
    -            source_identity = getattr(source, name)
    -            assert isinstance(target_identity, str)
    -            assert target_identity == source_identity
    -            distinct_source_identity = (source_identity + "#")[:-1]
    -            assert distinct_source_identity == source_identity
    -            assert distinct_source_identity is not target_identity
    -            setattr(source, name, distinct_source_identity)
    -
    -        old_catalogs = (target.background_job_catalog,)
    -        assert all(catalog is not None for catalog in old_catalogs)
    -        target.state = "validating"
    -
    -        plugin_manager_module._replace_snapshot_payload(  # pyright: ignore[reportPrivateUsage]
    -            target,
    -            source,
    -        )
    -
    -        # 3. catalog、identity 与 Root 必须来自同一份 formal snapshot。
    -        catalog_fields = ("background_job_catalog",)
    -        for name, old_catalog in zip(catalog_fields, old_catalogs, strict=True):
    -            catalog = getattr(target, name)
    -            assert catalog is getattr(source, name)
    -            assert catalog is not old_catalog
    -            assert catalog.root_instance_token is formal_root.instance_token
    -        for name in identity_fields:
    -            assert getattr(target, name) is getattr(source, name)
    -        assert target.composition_root is formal_root
    -        RuntimeSnapshotStore._validate_composition(  # pyright: ignore[reportPrivateUsage]
    -            target
    -        )
    -    finally:
    -        await validation_root.dispose()
    -        await formal_root.dispose()
    -
    -
    -def _channel_plugin_source(
    -    version: str,
    -    *,
    -    fail_start: bool = False,
    -    fail_stop: bool = False,
    -    block_deliver: bool = False,
    -) -> str:
    -    delivery_body = (
    -        "            DELIVERY_ENTERED.set()\n"
    -        "            await asyncio.Event().wait()\n"
    -        if block_deliver
    -        else "            return ProviderDeliveryReceipt(request.delivery_id, DeliveryStatus.DELIVERED)\n"
    -    )
    -    return (
    -        "import asyncio\n"
    -        "from pydantic import AliasChoices, BaseModel, Field\n"
    -        "from agent.plugin_composition import (\n"
    -        "    CHANNELS, ChannelCapability, ChannelDefinition, ChannelReady, CredentialRef,\n"
    -        "    DeliveryStatus, InboundIdentity, ProviderDeliveryReceipt, StopReceipt,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'channel_probe'\n"
    -        f"version = {version!r}\n"
    -        "DELIVERY_ENTERED = asyncio.Event()\n"
    -        "inject = (CHANNELS,)\n"
    -        "class Config(BaseModel):\n"
    -        "    app_id: str\n"
    -        "    app_secret: CredentialRef = Field(\n"
    -        "        validation_alias=AliasChoices('app_secret', 'appSecret'),\n"
    -        "    )\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(CHANNELS).register(ctx, ChannelDefinition(\n"
    -        "        name='feishu',\n"
    -        "        capabilities=frozenset({ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}),\n"
    -        "        factory_export='build_adapter',\n"
    -        "        inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,\n"
    -        "        credential_paths=('appSecret', 'app_secret'),\n"
    -        "    ))\n"
    -        "class Adapter:\n"
    -        "    def __init__(self, context):\n"
    -        "        self.context = context\n"
    -        "        self.ports = None\n"
    -        "        self.admission_open = False\n"
    -        "        self._in_flight = 0\n"
    -        "        self._drained = asyncio.Event()\n"
    -        "        self._drained.set()\n"
    -        f"        self.fail_start = {fail_start!r}\n"
    -        f"        self.fail_stop = {fail_stop!r}\n"
    -        "    def attach_runtime(self, ports):\n"
    -        "        if self.admission_open: raise RuntimeError('channel admission already open')\n"
    -        "        if ports.binding_token != self.context.binding_token: raise RuntimeError('channel binding mismatch')\n"
    -        "        if ports.ingress is None: raise RuntimeError('channel ingress missing')\n"
    -        "        self.ports = ports\n"
    -        "    def open_admission(self):\n"
    -        "        if self.ports is None: raise RuntimeError('channel runtime not attached')\n"
    -        "        self.admission_open = True\n"
    -        "    def close_admission(self):\n"
    -        "        self.admission_open = False\n"
    -        "    async def start(self):\n"
    -        "        if self.fail_start: raise RuntimeError('channel start failed')\n"
    -        "        if self.ports is None: raise RuntimeError('channel runtime not attached')\n"
    -        "        return ChannelReady(self.context.binding_token)\n"
    -        "    async def deliver(self, request):\n"
    -        "        self._in_flight += 1\n"
    -        "        self._drained.clear()\n"
    -        "        try:\n" + delivery_body + "        finally:\n"
    -        "            self._in_flight -= 1\n"
    -        "            if self._in_flight == 0: self._drained.set()\n"
    -        "    async def stop(self):\n"
    -        "        self.close_admission()\n"
    -        "        await self._drained.wait()\n"
    -        "        if self.fail_stop: raise RuntimeError('channel stop failed')\n"
    -        "        return StopReceipt(self.context.binding_token, True)\n"
    -        "def build_adapter(context): return Adapter(context)\n"
    -    )
    -
    -
    -def _channel_static_manifest(version: str) -> str:
    -    return (
    -        "schema_version = 1\n"
    -        "name = 'channel_probe'\n"
    -        f"version = {version!r}\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[channel_credentials]\n"
    -        "feishu = ['app_secret', 'appSecret']\n"
    -    )
    -
    -
    -def _write_static_v3_manifest(
    -    root: Path,
    -    name: str,
    -    version: str,
    -) -> None:
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        f"name = {name!r}\n"
    -        f"version = {version!r}\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n",
    -        encoding="utf-8",
    -    )
    -
    -
    -def _shared_writer_source(version: str) -> str:
    -    return f"""\
    -api_version = 3
    -name = 'shared_writer'
    -version = {version!r}
    -import asyncio
    -import sqlite3
    -from agent.plugin_composition import RUNTIME_STARTED, RUNTIME_STOPPING
    -writer_task = None
    -
    -async def apply(ctx, config):
    -    database = ctx.data_root / 'writer.sqlite3'
    -    root_token = id(ctx._root_instance_token())
    -    ctx.data_root.mkdir(parents=True, exist_ok=True)
    -    connection = sqlite3.connect(database)
    -    connection.execute(
    -        'CREATE TABLE IF NOT EXISTS owner ('
    -        'slot INTEGER PRIMARY KEY CHECK (slot = 1), version TEXT NOT NULL)'
    -    )
    -    connection.execute(
    -        'CREATE TABLE IF NOT EXISTS trace ('
    -        'seq INTEGER PRIMARY KEY AUTOINCREMENT, event TEXT NOT NULL)'
    -    )
    -    connection.execute(
    -        'CREATE TABLE IF NOT EXISTS writes ('
    -        'seq INTEGER PRIMARY KEY AUTOINCREMENT, version TEXT NOT NULL, '
    -        'root_token INTEGER NOT NULL)'
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -    async def started(_event):
    -        global writer_task
    -        connection = sqlite3.connect(database)
    -        connection.execute('INSERT INTO owner VALUES (1, ?)', ({version!r},))
    -        connection.execute('INSERT INTO trace(event) VALUES (?)', ('start:{version}',))
    -        connection.commit()
    -        connection.close()
    -
    -        async def write_forever():
    -            connection = sqlite3.connect(database)
    -            try:
    -                while True:
    -                    connection.execute(
    -                        'INSERT INTO writes(version, root_token) VALUES (?, ?)',
    -                        ({version!r}, root_token),
    -                    )
    -                    connection.commit()
    -                    await asyncio.sleep(0)
    -            finally:
    -                connection.close()
    -
    -        writer_task = asyncio.create_task(write_forever())
    -
    -    async def stopping(_event):
    -        global writer_task
    -        if writer_task is not None:
    -            writer_task.cancel()
    -            try:
    -                await writer_task
    -            except asyncio.CancelledError:
    -                pass
    -            writer_task = None
    -        connection = sqlite3.connect(database)
    -        connection.execute('DELETE FROM owner WHERE version = ?', ({version!r},))
    -        connection.execute('INSERT INTO trace(event) VALUES (?)', ('stop:{version}',))
    -        connection.commit()
    -        connection.close()
    -
    -    await ctx.on(RUNTIME_STARTED, started)
    -    await ctx.on(RUNTIME_STOPPING, stopping)
    -"""
    -
    -
    -def _sqlite_scalar(database: Path, query: str) -> object:
    -    connection = sqlite3.connect(database)
    -    try:
    -        return connection.execute(query).fetchone()[0]
    -    finally:
    -        connection.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_uses_isolated_data_copy(
    -    tmp_path: Path,
    -) -> None:
    -    _ = _write_plugin(
    -        tmp_path / "plugins",
    -        "isolated_reader",
    -        "api_version = 3\n"
    -        "name = 'isolated_reader'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    ctx.data_root.mkdir(parents=True, exist_ok=True)\n"
    -        "    (ctx.data_root / 'isolated.txt').write_text('isolated')\n",
    -    )
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "shared_reader",
    -        "api_version = 3\n"
    -        "name = 'shared_reader'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    import sqlite3\n"
    -        "    ctx.data_root.mkdir(parents=True, exist_ok=True)\n"
    -        "    connection = sqlite3.connect(ctx.data_root / 'state.sqlite3')\n"
    -        "    connection.execute('CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY)')\n"
    -        "    connection.execute('INSERT INTO items DEFAULT VALUES')\n"
    -        "    connection.commit()\n"
    -        "    connection.close()\n",
    -    )
    -    _write_static_v3_manifest(plugin_dir, "shared_reader", "1.0.0")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.generation("shared_reader")
    -    isolated = manager.generation("isolated_reader")
    -    assert stable is not None and isolated is not None
    -    assert stable.source_type == "builtin"
    -    assert stable.static_manifest is not None
    -    database = stable.data_dir / "state.sqlite3"
    -    sparse = stable.data_dir / "large.sparse"
    -    with sparse.open("wb") as stream:
    -        stream.truncate(1024 * 1024)
    -    formal_inode = database.stat().st_ino
    -    formal_digest = hashlib.sha256(database.read_bytes()).hexdigest()
    -    proactive = tmp_path / "workspace" / "proactive.db"
    -    wake_proactive = tmp_path / "workspace" / "wake_proactive.db"
    -    proactive.write_bytes(b"legacy proactive island")
    -    wake_proactive.write_bytes(b"legacy wake island")
    -    proactive_inode = proactive.stat().st_ino
    -    wake_proactive_inode = wake_proactive.stat().st_ino
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'shared_reader'\n"
    -        "version = '2.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    import json, sqlite3\n"
    -        "    (ctx.data_root / 'candidate-marker').write_text('candidate')\n"
    -        "    database = ctx.data_root / 'state.sqlite3'\n"
    -        "    connection = sqlite3.connect(f'file:{database}?mode=ro', uri=True)\n"
    -        "    rows = connection.execute('SELECT COUNT(*) FROM items').fetchone()[0]\n"
    -        "    rejected = False\n"
    -        "    try:\n"
    -        "        connection.execute('INSERT INTO items DEFAULT VALUES')\n"
    -        "    except sqlite3.OperationalError:\n"
    -        "        rejected = True\n"
    -        "    connection.close()\n"
    -        "    ctx.runtime.workspace.mkdir(parents=True, exist_ok=True)\n"
    -        "    (ctx.runtime.workspace / 'shared-read.json').write_text(\n"
    -        "        json.dumps({'rows': rows, 'write_rejected': rejected})\n"
    -        "    )\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(plugin_dir, "shared_reader", "2.0.0")
    -
    -    candidate = await manager.prepare_candidate("shared_reader")
    -
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    assert candidate.validation_workspace is not None
    -    assert "state.sqlite3" in candidate.validation_data_inventory
    -    assert "large.sparse" in candidate.validation_data_inventory
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_fibers = {
    -        fiber.name: fiber for fiber in candidate_root.root_fiber.children
    -    }
    -    candidate_runtime = candidate_fibers["shared_reader"].runtime
    -    assert candidate_runtime is not None
    -    assert candidate_runtime.data_dir != stable.data_dir
    -    assert "isolated_reader" not in candidate_fibers
    -    assert (isolated.data_dir / "isolated.txt").read_text() == "isolated"
    -    validation_root = candidate.validation_workspace.parent
    -    observations = tuple(validation_root.rglob("shared-read.json"))
    -    assert len(observations) == 1
    -    assert json.loads(observations[0].read_text(encoding="utf-8")) == {
    -        "rows": 1,
    -        "write_rejected": True,
    -    }
    -    assert len(tuple(validation_root.rglob("state.sqlite3"))) == 1
    -    assert len(tuple(validation_root.rglob("large.sparse"))) == 1
    -    assert len(tuple(validation_root.rglob("candidate-marker"))) == 1
    -    assert not tuple(validation_root.rglob("proactive.db"))
    -    assert not tuple(validation_root.rglob("wake_proactive.db"))
    -    assert not (stable.data_dir / "candidate-marker").exists()
    -    assert database.stat().st_ino == formal_inode
    -    assert hashlib.sha256(database.read_bytes()).hexdigest() == formal_digest
    -    assert proactive.stat().st_ino == proactive_inode
    -    assert wake_proactive.stat().st_ino == wake_proactive_inode
    -    formal = sqlite3.connect(database)
    -    try:
    -        assert formal.execute("SELECT COUNT(*) FROM items").fetchone() == (1,)
    -    finally:
    -        formal.close()
    -
    -    await manager.discard_prepared("shared_reader")
    -    assert not validation_root.exists()
    -    assert database.is_file() and sparse.is_file()
    -    assert proactive.is_file() and wake_proactive.is_file()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_unrelated_candidate_does_not_mount_or_copy_stateful_plugin(
    -    tmp_path: Path,
    -) -> None:
    -    stateful_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "stateful",
    -        "api_version = 3\n"
    -        "name = 'stateful'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memory',)\n"
    -        "workspace_files = ('sessions.db',)\n"
    -        "async def apply(ctx, config):\n"
    -        "    ctx.data_root.mkdir(parents=True, exist_ok=True)\n"
    -        "    marker = ctx.data_root / 'mount-count'\n"
    -        "    count = int(marker.read_text()) if marker.exists() else 0\n"
    -        "    marker.write_text(str(count + 1))\n"
    -        "    writer = ctx.data_root / 'exclusive-writer'\n"
    -        "    if writer.exists():\n"
    -        "        raise RuntimeError('stateful writer already mounted')\n"
    -        "    writer.write_text(ctx.runtime.generation_id)\n"
    -        "    def cleanup():\n"
    -        "        writer.unlink()\n"
    -        "    await ctx.effect(lambda: cleanup, label='exclusive-writer')\n",
    -    )
    -    candidate_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "candidate_only",
    -        "api_version = 3\n"
    -        "name = 'candidate_only'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -    )
    -    _ = stateful_dir
    -    workspace = tmp_path / "workspace"
    -    (workspace / "memory").mkdir(parents=True)
    -    (workspace / "memory" / "large-index").write_bytes(b"do-not-copy")
    -    (workspace / "sessions.db").write_bytes(b"do-not-copy")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stateful = manager.generation("stateful")
    -    assert stateful is not None
    -    marker = stateful.data_dir / "mount-count"
    -    writer = stateful.data_dir / "exclusive-writer"
    -    assert marker.read_text() == "1"
    -    assert writer.is_file()
    -
    -    with (candidate_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -        handle.write("\n# candidate revision\n")
    -    candidate = await manager.prepare_candidate("candidate_only")
    -
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    root = candidate.runtime_snapshot.composition_root
    -    assert root is not None
    -    assert [fiber.name for fiber in root.root_fiber.children] == ["candidate_only"]
    -    assert marker.read_text() == "1"
    -    assert writer.is_file()
    -    validation_root = candidate.validation_workspace
    -    assert validation_root is not None
    -    assert not (validation_root / "memory").exists()
    -    assert not (validation_root / "sessions.db").exists()
    -    attempt_root = validation_root.parent
    -
    -    result = await manager.publish_prepared("candidate_only")
    -
    -    assert result["publication_state"] == "committed"
    -    assert marker.read_text() == "2"
    -    assert writer.is_file()
    -    assert not attempt_root.exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_with_unknown_service_is_rejected_before_latest(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "invalid_dependency",
    -        "api_version = 3\n"
    -        "name = 'invalid_dependency'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    plugin_dir.joinpath("plugin.py").write_text(
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "MISSING = ServiceKey('missing.service.v1')\n"
    -        "api_version = 3\n"
    -        "name = 'invalid_dependency'\n"
    -        "version = '2.0.0'\n"
    -        "inject = (MISSING,)\n"
    -        "async def apply(ctx, config): raise AssertionError('must stay pending')\n",
    -        encoding="utf-8",
    -    )
    -
    -    candidate = await manager.prepare_candidate("invalid_dependency")
    -
    -    assert candidate is None
    -    assert manager.current_snapshot is stable
    -    assert manager.prepared_generation("invalid_dependency") is None
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_cannot_remove_service_required_by_stable_plugin(
    -    tmp_path: Path,
    -) -> None:
    -    provider_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "provider",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "SHARED = ServiceKey('fixture.shared.v1')\n"
    -        "api_version = 3\n"
    -        "name = 'provider'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config): await ctx.provide(SHARED, object())\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "consumer",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "SHARED = ServiceKey('fixture.shared.v1')\n"
    -        "api_version = 3\n"
    -        "name = 'consumer'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (SHARED,)\n"
    -        "async def apply(ctx, config): ctx.require(SHARED)\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    provider_dir.joinpath("plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'provider'\n"
    -        "version = '2.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -
    -    candidate = await manager.prepare_candidate("provider")
    -
    -    assert candidate is None
    -    assert manager.current_snapshot is stable
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("owner_commit_fails", [False, True])
    -async def test_isolated_candidate_publish_drains_old_writer_before_new_start(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    owner_commit_fails: bool,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "shared_writer",
    -        _shared_writer_source("v1"),
    -    )
    -    _write_static_v3_manifest(plugin_dir, "shared_writer", "v1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.generation("shared_writer")
    -    stable_snapshot = manager.current_snapshot
    -    assert stable is not None and stable_snapshot is not None
    -    assert stable_snapshot.composition_root is not None
    -    old_root_token = id(stable_snapshot.composition_root.instance_token)
    -    database = stable.data_dir / "writer.sqlite3"
    -    runtime_services = asyncio.create_task(manager.run_runtime_services())
    -    while stable.instance.module.writer_task is None:
    -        await asyncio.sleep(0)
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _shared_writer_source("v2"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(plugin_dir, "shared_writer", "v2")
    -    candidate = await manager.prepare_candidate("shared_writer")
    -    assert candidate is not None
    -    assert candidate.instance.module.writer_task is None
    -    assert candidate.runtime_snapshot is not None
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    if owner_commit_fails:
    -
    -        def fail_owner_commit(*_args: object) -> None:
    -            raise RuntimeError("candidate owner commit failed")
    -
    -        monkeypatch.setattr(
    -            manager,
    -            "_activate_published_generation",
    -            fail_owner_commit,
    -        )
    -
    -    # 1. An accepted old Turn keeps the old writer alive while publication waits.
    -    old_lease = await manager.snapshot_store.acquire()
    -    publication = asyncio.create_task(manager.publish_prepared("shared_writer"))
    -    while stable_snapshot.accepting_leases:
    -        await asyncio.sleep(0)
    -    before_wait = cast(
    -        int,
    -        _sqlite_scalar(
    -            database,
    -            "SELECT COUNT(*) FROM writes "
    -            f"WHERE version = 'v1' AND root_token = {old_root_token}",
    -        ),
    -    )
    -    for _ in range(20):
    -        await asyncio.sleep(0)
    -    during_wait = cast(
    -        int,
    -        _sqlite_scalar(
    -            database,
    -            "SELECT COUNT(*) FROM writes "
    -            f"WHERE version = 'v1' AND root_token = {old_root_token}",
    -        ),
    -    )
    -    assert during_wait > before_wait
    -    waiting_admission = asyncio.create_task(manager.snapshot_store.acquire())
    -    await asyncio.sleep(0)
    -    assert not publication.done()
    -    assert not waiting_admission.done()
    -
    -    # 2. Releasing the Turn lets STOPPING settle v1 before v2 receives STARTED.
    -    await old_lease.release()
    -    if owner_commit_fails:
    -        with pytest.raises(RuntimeError, match="candidate owner commit failed"):
    -            await publication
    -        result = None
    -    else:
    -        result = await publication
    -    new_lease = await waiting_admission
    -    await new_lease.release()
    -    if result is not None:
    -        assert result["publication_state"] == "committed"
    -    connection = sqlite3.connect(database)
    -    trace = [
    -        row[0] for row in connection.execute("SELECT event FROM trace ORDER BY seq")
    -    ]
    -    owners = connection.execute("SELECT version FROM owner").fetchall()
    -    old_writes = connection.execute(
    -        "SELECT COUNT(*) FROM writes WHERE version = 'v1' AND root_token = ?",
    -        (old_root_token,),
    -    ).fetchone()[0]
    -    connection.close()
    -    if owner_commit_fails:
    -        assert trace == [
    -            "start:v1",
    -            "stop:v1",
    -            "start:v1",
    -        ]
    -        assert owners == [("v1",)]
    -        assert candidate.instance.module.writer_task is None
    -        assert stable.instance.module.writer_task is not None
    -    else:
    -        assert trace == ["start:v1", "stop:v1", "start:v2"]
    -        assert owners == [("v2",)]
    -        assert stable.instance.module.writer_task is None
    -
    -    # 3. The terminal old Root cannot write again after the new writer is open.
    -    for _ in range(20):
    -        await asyncio.sleep(0)
    -    assert (
    -        _sqlite_scalar(
    -            database,
    -            "SELECT COUNT(*) FROM writes "
    -            f"WHERE version = 'v1' AND root_token = {old_root_token}",
    -        )
    -        == old_writes
    -    )
    -    runtime_services.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await runtime_services
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_rebuild_rejects_candidate_topology_drift(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "shared_writer",
    -        _shared_writer_source("v1"),
    -    )
    -    _write_static_v3_manifest(plugin_dir, "shared_writer", "v1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.generation("shared_writer")
    -    stable_snapshot = manager.current_snapshot
    -    assert stable is not None and stable_snapshot is not None
    -    old_root = stable_snapshot.composition_root
    -    assert old_root is not None
    -    runtime_services = asyncio.create_task(manager.run_runtime_services())
    -    while old_root.instance_token not in manager._runtime_started_roots:
    -        await asyncio.sleep(0)
    -
    -    mutant = _shared_writer_source("v2").replace(
    -        "    await ctx.on(RUNTIME_STOPPING, stopping)\n",
    -        "    await ctx.on(RUNTIME_STOPPING, stopping)\n"
    -        "    if 'plugin-validation' not in str(ctx.data_root):\n"
    -        "        await ctx.on(RUNTIME_STARTED, lambda _: None)\n",
    -    )
    -    (plugin_dir / "plugin.py").write_text(mutant, encoding="utf-8")
    -    _write_static_v3_manifest(plugin_dir, "shared_writer", "v2")
    -    candidate = await manager.prepare_candidate("shared_writer")
    -    assert candidate is not None
    -
    -    with pytest.raises(RuntimeError, match="snapshot identity"):
    -        await manager.publish_prepared("shared_writer")
    -
    -    replacement_root = stable_snapshot.composition_root
    -    assert replacement_root is not None and replacement_root is not old_root
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.generation("shared_writer") is stable
    -    assert stable_snapshot.accepting_leases
    -    assert manager.prepared_generation("shared_writer") is None
    -    runtime_services.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await runtime_services
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_registry_redacts_candidate_credentials_before_import(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    secret = "candidate-must-never-read-this-secret"
    -    original_config = f"app_id = 'app-1'\nappSecret = '{secret}'\n".encode()
    -    config_path.write_bytes(original_config)
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    stable = manager.current_snapshot
    -    generation = manager.generation("channel_probe")
    -    assert stable is not None and generation is not None
    -    assert stable.channel_registry is not None
    -    assert manager.stable_channel_catalog() is stable.channel_registry
    -    assert stable.channel_registry.descriptors[0].credential_paths == (
    -        "appSecret",
    -        "app_secret",
    -    )
    -    assert isinstance(generation.config.app_secret, CredentialRef)  # type: ignore[union-attr]
    -    assert config_path.read_bytes() == original_config
    -    runtime = _active_channel_generation(manager)
    -    assert runtime is not None and runtime.snapshot_id == stable.snapshot_id
    -    binding = runtime.channel("feishu")
    -    assert binding.admission_open is True
    -    assert generation.reload_tx_id is not None
    -    record = manager.reload_journal.get(generation.reload_tx_id)
    -    assert record.phase == "complete"
    -    evidence = repr(manager.reload_journal.events(generation.reload_tx_id))
    -    assert "channel_binding_reserved" in evidence
    -    assert secret not in evidence
    -
    -    other_root = CompositionRoot("other-channel-root")
    -    await other_root.context.provide(
    -        CHANNELS,
    -        PluginChannels(other_root.instance_token),
    -    )
    -
    -    async def register_other(ctx) -> None:
    -        await ctx.require(CHANNELS).register(
    -            ctx,
    -            ChannelDefinition(
    -                name="feishu",
    -                capabilities=frozenset(
    -                    {ChannelCapability.INBOUND, ChannelCapability.OUTBOUND}
    -                ),
    -                factory_export="build_adapter",
    -                inbound_identity=InboundIdentity.PROVIDER_MESSAGE_ID,
    -                credential_paths=("appSecret", "app_secret"),
    -            ),
    -        )
    -
    -    _ = await other_root.mount(
    -        register_other,
    -        name="channel_probe",
    -        runtime=PluginRuntime(
    -            plugin_id="channel_probe",
    -            generation_id="test-generation",
    -            plugin_dir=plugin_dir,
    -            data_dir=data_dir,
    -            workspace=tmp_path / "workspace",
    -            config=generation.config,
    -        ),
    -        inject=(CHANNELS,),
    -    )
    -    other_snapshot = RuntimeSnapshotCompiler().compile(
    -        {"channel_probe": generation},
    -        composition_root=other_root,
    -    )
    -    assert other_snapshot.channel_registry_identity == stable.channel_registry_identity
    -    other_snapshot.channel_registry = stable.channel_registry
    -    with pytest.raises(RuntimeError, match="不属于 exact Root"):
    -        RuntimeSnapshotStore().install(other_snapshot)
    -    await other_root.dispose()
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _channel_plugin_source("2.0.0"),
    -        encoding="utf-8",
    -    )
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -    candidate = await manager.prepare_candidate("channel_probe")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    assert candidate.validation_workspace is not None
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_runtime = candidate_root.root_fiber.children[0].runtime
    -    assert candidate_runtime is not None
    -    assert isinstance(candidate_runtime.config.app_secret, CredentialRef)
    -    assert "config.local.toml" not in candidate.validation_data_inventory
    -    validation_root = candidate.validation_workspace.parent
    -    for path in validation_root.rglob("*"):
    -        if path.is_file() and not path.is_symlink():
    -            assert secret.encode() not in path.read_bytes()
    -    assert config_path.read_bytes() == original_config
    -
    -    await manager.discard_prepared("channel_probe")
    -    assert not validation_root.exists()
    -    assert manager.current_snapshot is stable
    -    assert config_path.read_bytes() == original_config
    -
    -    promoted = await manager.prepare_candidate("channel_probe")
    -    assert promoted is not None
    -    held_stable = manager.snapshot_store.lease()
    -    publication = asyncio.create_task(manager.publish_prepared("channel_probe"))
    -    await asyncio.sleep(0)
    -    assert not publication.done()
    -    assert runtime.channel("feishu").admission_open
    -    await held_stable.release()
    -    result = await publication
    -    assert result["publication_state"] == "committed"
    -    current = manager.current_snapshot
    -    active_runtime = _active_channel_generation(manager)
    -    assert current is not None and current is not stable
    -    assert active_runtime is not None
    -    assert active_runtime.snapshot_id == current.snapshot_id
    -    assert active_runtime.channel("feishu").admission_open
    -    assert config_path.read_bytes() == original_config
    -    await manager.terminate_all()
    -    assert _active_channel_generation(manager) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_unrelated_snapshot_publication_rebinds_exact_channel_runtime(
    -    tmp_path: Path,
    -) -> None:
    -    """非 Channel 插件晋升后,入站 runtime 必须绑定新的 exact snapshot。"""
    -
    -    # 1. 启动一个 Channel 与一个不贡献 Channel 的普通插件
    -    channel_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    (channel_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'secret'\n",
    -        encoding="utf-8",
    -    )
    -    plain_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "plain_probe",
    -        "api_version = 3\n"
    -        "name = 'plain_probe'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    previous = manager.current_snapshot
    -    previous_runtime = _active_channel_generation(manager)
    -    assert previous is not None and previous_runtime is not None
    -
    -    # 2. 只晋升普通插件,Channel catalog identity 保持不变
    -    (plain_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'plain_probe'\n"
    -        "version = '2.0.0'\n"
    -        "async def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    assert await manager.prepare_candidate("plain_probe") is not None
    -    result = await manager.publish_prepared("plain_probe")
    -    current = manager.current_snapshot
    -    current_runtime = _active_channel_generation(manager)
    -
    -    # 3. 新入站只能租用新 snapshot,旧 binding 已完成排空
    -    assert result["publication_state"] == "committed"
    -    assert current is not None and current is not previous
    -    assert current.channel_registry_identity == previous.channel_registry_identity
    -    assert current_runtime is not None and current_runtime is not previous_runtime
    -    assert current_runtime.snapshot_id == current.snapshot_id
    -    assert current_runtime.channel("feishu").admission_open
    -    lease = manager.snapshot_store.lease(current_runtime.snapshot_id)
    -    await lease.release()
    -    with pytest.raises(RuntimeError, match="RuntimeSnapshot 不可(用|租用)"):
    -        manager.snapshot_store.lease(previous_runtime.snapshot_id)
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_manager_binds_core_attachment_ports(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    workspace = tmp_path / "workspace"
    -    data_dir = workspace / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'secret'\n",
    -        encoding="utf-8",
    -    )
    -    session_store = SessionStore(workspace / "sessions.db")
    -    attachment_store = ChannelAttachmentArtifactStore(
    -        workspace=workspace,
    -        session_store=session_store,
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "home" / "cache",
    -        channel_attachment_store=attachment_store,
    -    )
    -    try:
    -        await manager.load_all()
    -        runtime = _active_channel_generation(manager)
    -        assert runtime is not None
    -        state = cast(Any, manager.channel_generation_host)._bindings[
    -            (runtime.snapshot_id, "feishu")
    -        ]
    -        context = state.factory_context
    -        assert context is not None
    -        assert context.attachment_import is not None
    -        assert context.attachment_read is not None
    -        adapter = state.adapter
    -        assert adapter is not None
    -        assert adapter.ports.binding_token == state.binding_token
    -        assert adapter.ports.ingress is context.ingress
    -        assert adapter.admission_open is True
    -
    -        ref = await context.attachment_import.import_bytes(
    -            b"manager-bound attachment",
    -            kind=AttachmentKind.FILE,
    -            filename="evidence.txt",
    -            media_type="text/plain",
    -        )
    -        lease = await context.attachment_read.acquire(ref)
    -        assert await lease.read_bytes(max_bytes=1024) == b"manager-bound attachment"
    -        await lease.aclose()
    -    finally:
    -        await manager.terminate_all()
    -        session_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_direct_push_uses_exact_stable_binding(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'secret'\n",
    -        encoding="utf-8",
    -    )
    -    workspace = tmp_path / "workspace"
    -    session_store = SessionStore(workspace / "sessions.db")
    -    attachment_store = ChannelAttachmentArtifactStore(
    -        workspace=workspace,
    -        session_store=session_store,
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "home" / "cache",
    -        channel_attachment_store=attachment_store,
    -    )
    -    await manager.load_all()
    -    bus = MessageBus()
    -    bus.bind_channel_outbound_dispatcher(
    -        manager.channel_generation_host.dispatch_outbound
    -    )
    -    dispatch_task = asyncio.create_task(bus.dispatch_outbound())
    -    tool = MessagePushTool(chat_lane=bus.chat_lane)
    -    tool.bind_v3_channel_dispatcher(
    -        lambda message, passive: _dispatch_v3_channel_push(
    -            manager,
    -            bus,
    -            message,
    -            passive,
    -            attachment_store,
    -        )
    -    )
    -
    -    source = await manager.snapshot_store.acquire()
    -    snapshot_token = bind_runtime_snapshot(source)
    -
    -    async def reject_stable_reacquire(*args: object, **kwargs: object) -> object:
    -        raise AssertionError("direct push 必须复用当前 exact snapshot lease")
    -
    -    monkeypatch.setattr(manager.snapshot_store, "acquire", reject_stable_reacquire)
    -
    -    image = tmp_path / "image.png"
    -    image.write_bytes(b"channel image")
    -    try:
    -        delivered = json.loads(
    -            await asyncio.wait_for(
    -                tool.execute(
    -                    target_channel="feishu",
    -                    target_chat_id="ou_1",
    -                    message="hello",
    -                ),
    -                timeout=1,
    -            )
    -        )
    -        attached = json.loads(
    -            await asyncio.wait_for(
    -                tool.execute(
    -                    target_channel="feishu",
    -                    target_chat_id="ou_1",
    -                    image=str(image),
    -                ),
    -                timeout=1,
    -            )
    -        )
    -    finally:
    -        reset_runtime_snapshot(snapshot_token)
    -        await source.release()
    -
    -    assert delivered["status"] == "delivered"
    -    assert delivered["retryable"] is False
    -    assert attached["status"] == "delivered"
    -    assert attached["retryable"] is False
    -    assert len(session_store.list_attachments()) == 1
    -    runtime = _active_channel_generation(manager)
    -    assert runtime is not None
    -    assert runtime.channel("feishu").in_flight == 0
    -
    -    await bus.aclose()
    -    dispatch_task.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await dispatch_task
    -    await manager.terminate_all()
    -    session_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_direct_push_bus_close_settles_unknown_and_releases_binding(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0", block_deliver=True),
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    generation = manager.generation("channel_probe")
    -    assert generation is not None
    -    module = cast(ComposablePlugin, generation.instance).module
    -    entered = cast(asyncio.Event, module.DELIVERY_ENTERED)
    -    bus = MessageBus()
    -    bus.bind_channel_outbound_dispatcher(
    -        manager.channel_generation_host.dispatch_outbound
    -    )
    -    dispatch_task = asyncio.create_task(bus.dispatch_outbound())
    -    tool = MessagePushTool(chat_lane=bus.chat_lane)
    -    tool.bind_v3_channel_dispatcher(
    -        lambda message, passive: _dispatch_v3_channel_push(
    -            manager,
    -            bus,
    -            message,
    -            passive,
    -        )
    -    )
    -
    -    pending = asyncio.create_task(
    -        tool.execute(
    -            target_channel="feishu",
    -            target_chat_id="ou_1",
    -            message="hello",
    -        )
    -    )
    -    await asyncio.wait_for(entered.wait(), timeout=1)
    -    await asyncio.wait_for(bus.aclose(), timeout=1)
    -    result = json.loads(await asyncio.wait_for(pending, timeout=1))
    -
    -    assert result["status"] == "unknown"
    -    assert result["retryable"] is False
    -    runtime = _active_channel_generation(manager)
    -    assert runtime is not None
    -    assert runtime.channel("feishu").in_flight == 0
    -    assert dispatch_task.done()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_formal_start_rejects_raw_config_drift_before_factory(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    config_path.write_text(
    -        "app_id = 'app-1'\napp_secret = 'secret-before-seal'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    class ProviderFactory:
    -        closed = 0
    -
    -        async def create(self, credentials: Any) -> object:
    -            raise AssertionError("config drift 后不得创建 provider client")
    -
    -        async def aclose(self) -> None:
    -            self.closed += 1
    -
    -    provider = ProviderFactory()
    -
    -    def drift_after_snapshot(snapshot: Any) -> Mapping[str, ProviderClientFactory]:
    -        config_path.write_text(
    -            "app_id = 'app-1'\napp_secret = 'secret-after-seal'\n",
    -            encoding="utf-8",
    -        )
    -        return {"feishu": cast(ProviderClientFactory, provider)}
    -
    -    manager.bind_channel_provider_factory_resolver(drift_after_snapshot)
    -    with pytest.raises(RuntimeError, match="config revision 已漂移"):
    -        await manager.load_all()
    -
    -    assert provider.closed == 1
    -    assert manager.current_snapshot is None
    -    assert _active_channel_generation(manager) is None
    -    record = manager.reload_journal.latest(plugin_id="channel_probe")
    -    assert record is not None and record.phase == "aborted"
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_candidate_start_failure_restores_closed_stable_owner(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    config_path.write_text(
    -        "app_id = 'app-1'\napp_secret = 'formal-secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    class ProviderFactory:
    -        async def create(self, credentials: Any) -> object:
    -            return object()
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    manager.bind_channel_provider_factory_resolver(
    -        lambda snapshot: {
    -            descriptor.name: cast(ProviderClientFactory, ProviderFactory())
    -            for descriptor in cast(Any, snapshot.channel_registry).descriptors
    -        }
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    stable_runtime = _active_channel_generation(manager)
    -    assert stable is not None and stable_runtime is not None
    -    stable_token = stable_runtime.channel("feishu").binding_token
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _channel_plugin_source("2.0.0"),
    -        encoding="utf-8",
    -    )
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -    candidate = await manager.prepare_candidate("channel_probe")
    -    assert candidate is not None
    -    original_start = manager.channel_generation_host.start_formal
    -    failed = False
    -
    -    async def fail_candidate_once(snapshot: Any, factories: Any, **kwargs: Any):
    -        nonlocal failed
    -        if snapshot is not stable and not failed:
    -            failed = True
    -            assert manager.current_snapshot is stable
    -            assert manager.latest_snapshot is snapshot
    -            assert not stable.accepting_leases
    -            assert not snapshot.accepting_leases
    -            raise RuntimeError("candidate channel start failed")
    -        return await original_start(snapshot, factories, **kwargs)
    -
    -    monkeypatch.setattr(
    -        manager.channel_generation_host,
    -        "start_formal",
    -        fail_candidate_once,
    -    )
    -    with pytest.raises(RuntimeError, match="candidate channel start failed"):
    -        await manager.publish_prepared("channel_probe")
    -
    -    assert failed
    -    assert manager.current_snapshot is stable
    -    assert stable.accepting_leases
    -    restored = _active_channel_generation(manager)
    -    assert restored is not None and restored.snapshot_id == stable.snapshot_id
    -    assert restored.channel("feishu").admission_open
    -    assert restored.channel("feishu").binding_token != stable_token
    -    assert config_path.read_text(encoding="utf-8").endswith("formal-secret'\n")
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_old_restart_failure_keeps_durable_recovery_owner(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'formal-secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    endpoint_resume_calls = 0
    -
    -    async def quiesce_endpoints() -> None:
    -        return None
    -
    -    async def reject_unowned_resume() -> None:
    -        nonlocal endpoint_resume_calls
    -        endpoint_resume_calls += 1
    -        raise AssertionError("pure v3 Channel recovery 不拥有 endpoint admission")
    -
    -    manager.bind_endpoint_admission(
    -        quiesce=quiesce_endpoints,
    -        resume=reject_unowned_resume,
    -    )
    -
    -    class ProviderFactory:
    -        async def create(self, credentials: Any) -> object:
    -            return object()
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    manager.bind_channel_provider_factory_resolver(
    -        lambda snapshot: {
    -            descriptor.name: cast(ProviderClientFactory, ProviderFactory())
    -            for descriptor in cast(Any, snapshot.channel_registry).descriptors
    -        }
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _channel_plugin_source("2.0.0"),
    -        encoding="utf-8",
    -    )
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -    candidate = await manager.prepare_candidate("channel_probe")
    -    assert candidate is not None and candidate.reload_tx_id is not None
    -    original_start = manager.channel_generation_host.start_formal
    -    candidate_failed = False
    -
    -    async def fail_candidate_and_rollback(
    -        snapshot: Any,
    -        factories: Any,
    -        **kwargs: Any,
    -    ):
    -        nonlocal candidate_failed
    -        if snapshot is not stable and not candidate_failed:
    -            candidate_failed = True
    -            raise RuntimeError("candidate channel start failed")
    -        if kwargs.get("boot_owner") == "plugin-manager-rollback":
    -            raise RuntimeError("rollback channel restart failed")
    -        return await original_start(snapshot, factories, **kwargs)
    -
    -    monkeypatch.setattr(
    -        manager.channel_generation_host,
    -        "start_formal",
    -        fail_candidate_and_rollback,
    -    )
    -    with pytest.raises(RuntimeError, match="旧 owner 恢复失败"):
    -        await manager.publish_prepared("channel_probe")
    -
    -    assert manager.current_snapshot is stable
    -    assert not stable.accepting_leases
    -    assert _active_channel_generation(manager) is None
    -    record = manager.reload_journal.get(candidate.reload_tx_id)
    -    assert record.phase == "degraded"
    -    assert record.failure_resource == (f"channel-publication:{candidate.generation_id}")
    -
    -    recovered = await manager.retry_runtime_recovery("channel_probe")
    -    assert recovered["publication_state"] == "recovered"
    -    assert endpoint_resume_calls == 0
    -    assert stable.accepting_leases
    -    active = _active_channel_generation(manager)
    -    assert active is not None and active.channel("feishu").admission_open
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_old_stop_failure_keeps_stable_closed_until_exact_retry(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0", fail_stop=True),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'formal-secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    class ProviderFactory:
    -        async def create(self, credentials: Any) -> object:
    -            return object()
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    manager.bind_channel_provider_factory_resolver(
    -        lambda snapshot: {
    -            descriptor.name: cast(ProviderClientFactory, ProviderFactory())
    -            for descriptor in cast(Any, snapshot.channel_registry).descriptors
    -        }
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    runtime = _active_channel_generation(manager)
    -    assert stable is not None and runtime is not None
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _channel_plugin_source("2.0.0"),
    -        encoding="utf-8",
    -    )
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -    candidate = await manager.prepare_candidate("channel_probe")
    -    assert candidate is not None and candidate.reload_tx_id is not None
    -    with pytest.raises(RuntimeError, match="旧 owner 恢复失败"):
    -        await manager.publish_prepared("channel_probe")
    -
    -    assert manager.current_snapshot is stable
    -    assert not stable.accepting_leases
    -    assert not runtime.channel("feishu").admission_open
    -    failure = manager.channel_generation_host.failure(
    -        runtime.snapshot_id,
    -        "feishu",
    -    )
    -    assert failure is not None
    -    record = manager.reload_journal.get(candidate.reload_tx_id)
    -    assert record.phase == "degraded"
    -    assert record.failure_resource is not None
    -    assert set(record.failure_resource.split(",")) == {
    -        f"channel-binding:{failure.binding_token}",
    -        f"channel-publication:{candidate.generation_id}",
    -    }
    -
    -    state = next(
    -        value
    -        for key, value in cast(Any, manager.channel_generation_host)._bindings.items()
    -        if key[0] == runtime.snapshot_id
    -    )
    -    state.adapter.fail_stop = False
    -    recovered = await manager.retry_runtime_recovery("channel_probe")
    -    assert recovered["publication_state"] == "recovered"
    -    assert stable.accepting_leases
    -    active = _active_channel_generation(manager)
    -    assert active is not None and active.channel("feishu").admission_open
    -    assert manager.channel_generation_host.failure(runtime.snapshot_id) is None
    -    restored_state = next(
    -        value
    -        for key, value in cast(Any, manager.channel_generation_host)._bindings.items()
    -        if key[0] == active.snapshot_id
    -    )
    -    restored_state.adapter.fail_stop = False
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_candidate_cleanup_failure_blocks_old_restore_until_retry(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'formal-secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    class ProviderFactory:
    -        async def create(self, credentials: Any) -> object:
    -            return object()
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    manager.bind_channel_provider_factory_resolver(
    -        lambda snapshot: {
    -            descriptor.name: cast(ProviderClientFactory, ProviderFactory())
    -            for descriptor in cast(Any, snapshot.channel_registry).descriptors
    -        }
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        _channel_plugin_source(
    -            "2.0.0",
    -            fail_start=True,
    -            fail_stop=True,
    -        ),
    -        encoding="utf-8",
    -    )
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -    candidate = await manager.prepare_candidate("channel_probe")
    -    assert (
    -        candidate is not None
    -        and candidate.reload_tx_id is not None
    -        and candidate.runtime_snapshot is not None
    -    )
    -    with pytest.raises(RuntimeError, match="旧 owner 恢复失败"):
    -        await manager.publish_prepared("channel_probe")
    -
    -    assert manager.current_snapshot is stable
    -    assert not stable.accepting_leases
    -    assert _active_channel_generation(manager) is None
    -    failure = manager.channel_generation_host.failure(
    -        candidate.runtime_snapshot.snapshot_id,
    -        "feishu",
    -    )
    -    assert failure is not None
    -    state = next(
    -        value
    -        for key, value in cast(Any, manager.channel_generation_host)._bindings.items()
    -        if key[0] == candidate.runtime_snapshot.snapshot_id
    -    )
    -    state.adapter.fail_stop = False
    -    recovered = await manager.retry_runtime_recovery("channel_probe")
    -    assert recovered["publication_state"] == "recovered"
    -    assert stable.accepting_leases
    -    active = _active_channel_generation(manager)
    -    assert active is not None and active.channel("feishu").admission_open
    -    assert (
    -        manager.channel_generation_host.failure(candidate.runtime_snapshot.snapshot_id)
    -        is None
    -    )
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_terminate_failure_retains_exact_owner_until_retry(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0", fail_stop=True),
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\napp_secret = 'formal-secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    class ProviderFactory:
    -        async def create(self, credentials: Any) -> object:
    -            return object()
    -
    -        async def aclose(self) -> None:
    -            return None
    -
    -    manager.bind_channel_provider_factory_resolver(
    -        lambda snapshot: {
    -            descriptor.name: cast(ProviderClientFactory, ProviderFactory())
    -            for descriptor in cast(Any, snapshot.channel_registry).descriptors
    -        }
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    runtime = _active_channel_generation(manager)
    -    assert stable is not None and runtime is not None
    -
    -    with pytest.raises(RuntimeError, match="generation owner 已保留"):
    -        await manager.terminate_all()
    -
    -    assert manager.current_snapshot is stable
    -    assert not stable.accepting_leases
    -    assert manager.generation("channel_probe") is not None
    -    failure = manager.channel_generation_host.failure(
    -        runtime.snapshot_id,
    -        "feishu",
    -    )
    -    assert failure is not None
    -    state = next(
    -        value
    -        for key, value in cast(Any, manager.channel_generation_host)._bindings.items()
    -        if key[0] == runtime.snapshot_id
    -    )
    -    state.adapter.fail_stop = False
    -
    -    recovered = await manager.retry_runtime_recovery("channel_probe")
    -    assert recovered["publication_state"] == "recovered"
    -    active = _active_channel_generation(manager)
    -    assert active is not None and active.channel("feishu").admission_open
    -    restored_state = next(
    -        value
    -        for key, value in cast(Any, manager.channel_generation_host)._bindings.items()
    -        if key[0] == active.snapshot_id
    -    )
    -    restored_state.adapter.fail_stop = False
    -    await manager.terminate_all()
    -
    -
    -def test_v3_channel_secret_rejects_legacy_string_only_config_schema() -> None:
    -    class LegacyConfig(BaseModel):
    -        app_secret: str
    -
    -    with pytest.raises(
    -        plugin_manager_module._PluginConfigError,  # pyright: ignore[reportPrivateUsage]
    -        match="app_secret",
    -    ):
    -        plugin_manager_module._validate_plugin_config_projection(  # pyright: ignore[reportPrivateUsage]
    -            {"app_secret": CredentialRef(("app_secret",))},
    -            LegacyConfig,
    -        )
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("manifest_paths", "config_text"),
    -    (
    -        (("app_secret",), "app_id = 'app-1'\nappSecret = 'secret'\n"),
    -        (
    -            ("app_secret", "appSecret"),
    -            "app_id = 'app-1'\napp_secret = 'one'\nappSecret = 'two'\n",
    -        ),
    -    ),
    -)
    -async def test_v3_channel_credential_aliases_fail_before_apply(
    -    tmp_path: Path,
    -    manifest_paths: tuple[str, ...],
    -    config_text: str,
    -) -> None:
    -    marker = tmp_path / "candidate-apply-ran"
    -    source = _channel_plugin_source("1.0.0").replace(
    -        "async def apply(ctx, config):\n",
    -        "async def apply(ctx, config):\n"
    -        f"    __import__('pathlib').Path({str(marker)!r}).write_text('bad')\n",
    -    )
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "channel_probe", source)
    -    manifest = _channel_static_manifest("1.0.0").replace(
    -        "feishu = ['app_secret', 'appSecret']",
    -        f"feishu = {list(manifest_paths)!r}",
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(manifest, encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    original = config_text.encode()
    -    config_path.write_bytes(original)
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.generation("channel_probe") is None
    -    assert not marker.exists()
    -    assert config_path.read_bytes() == original
    -    assert not (tmp_path / "workspace" / "runtime" / "plugin-validation").exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_channel_credential_field_name_is_part_of_alias_admission(
    -    tmp_path: Path,
    -) -> None:
    -    validator_marker = tmp_path / "credential-before-validator-ran"
    -    apply_marker = tmp_path / "candidate-apply-ran"
    -    source = (
    -        "from pydantic import BaseModel, ConfigDict, Field, field_validator\n"
    -        "from agent.plugin_composition import (\n"
    -        "    CHANNELS, ChannelCapability, ChannelDefinition, CredentialRef, InboundIdentity,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'channel_probe'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (CHANNELS,)\n"
    -        "class Config(BaseModel):\n"
    -        "    model_config = ConfigDict(validate_by_name=True)\n"
    -        "    secret: CredentialRef = Field(validation_alias='appSecret')\n"
    -        "    @field_validator('secret', mode='before')\n"
    -        "    @classmethod\n"
    -        "    def observe_secret(cls, value):\n"
    -        f"        __import__('pathlib').Path({str(validator_marker)!r}).write_text(str(value))\n"
    -        "        return value\n"
    -        "async def apply(ctx, config):\n"
    -        f"    __import__('pathlib').Path({str(apply_marker)!r}).write_text('bad')\n"
    -        "    await ctx.require(CHANNELS).register(ctx, ChannelDefinition(\n"
    -        "        name='feishu',\n"
    -        "        capabilities=frozenset({ChannelCapability.OUTBOUND}),\n"
    -        "        factory_export='build_adapter',\n"
    -        "        inbound_identity=None,\n"
    -        "        credential_paths=('appSecret',),\n"
    -        "    ))\n"
    -    )
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "channel_probe", source)
    -    manifest = _channel_static_manifest("1.0.0").replace(
    -        "feishu = ['app_secret', 'appSecret']",
    -        "feishu = ['appSecret']",
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(manifest, encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    original = b"secret = 'candidate-must-never-see-this'\n"
    -    config_path.write_bytes(original)
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.generation("channel_probe") is None
    -    assert not validator_marker.exists()
    -    assert not apply_marker.exists()
    -    assert config_path.read_bytes() == original
    -    assert not (tmp_path / "workspace" / "runtime" / "plugin-validation").exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    "source_mutation,error",
    -    (
    -        (
    -            (
    -                "credential_paths=('appSecret', 'app_secret')",
    -                "credential_paths=('app_id',)",
    -            ),
    -            "credential 声明与静态 manifest 不一致",
    -        ),
    -        (
    -            ("inject = (CHANNELS,)", "inject = ()"),
    -            "静态 channel credential 没有对应 Root 声明",
    -        ),
    -    ),
    -)
    -async def test_v3_channel_manifest_and_root_declaration_must_match(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -    source_mutation: tuple[str, str],
    -    error: str,
    -) -> None:
    -    source = _channel_plugin_source("1.0.0")
    -    if source_mutation[0] == "inject = (CHANNELS,)":
    -        source = source[: source.index("async def apply")] + (
    -            "async def apply(ctx, config):\n" "    pass\n"
    -        )
    -    source = source.replace(*source_mutation)
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        source,
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        _channel_static_manifest("1.0.0"),
    -        encoding="utf-8",
    -    )
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    config_path = data_dir / "config.local.toml"
    -    original_config = b"app_id = 'app-1'\nappSecret = 'secret'\n"
    -    config_path.write_bytes(original_config)
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert error in caplog.text
    -    assert manager.current_snapshot is None
    -    assert manager.generation("channel_probe") is None
    -    assert config_path.read_bytes() == original_config
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_cannot_keep_channel_manifest_after_removing_declaration(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "channel_probe",
    -        _channel_plugin_source("1.0.0"),
    -    )
    -    manifest_path = plugin_dir / "akashic.plugin.toml"
    -    manifest_path.write_text(_channel_static_manifest("1.0.0"), encoding="utf-8")
    -    data_dir = tmp_path / "workspace" / "plugin-data" / "channel_probe-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        "app_id = 'app-1'\nappSecret = 'secret'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None and stable.channel_registry is not None
    -
    -    source = _channel_plugin_source("2.0.0")
    -    source = source[: source.index("async def apply")] + (
    -        "async def apply(ctx, config):\n    pass\n"
    -    )
    -    source = source.replace("inject = (CHANNELS,)", "inject = ()")
    -    (plugin_dir / "plugin.py").write_text(source, encoding="utf-8")
    -    manifest_path.write_text(_channel_static_manifest("2.0.0"), encoding="utf-8")
    -
    -    candidate = await manager.prepare_candidate("channel_probe")
    -
    -    assert candidate is None
    -    assert "候选验证失败: runtime_snapshot" in caplog.text
    -    assert manager.current_snapshot is stable
    -    assert manager.latest_snapshot is stable
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_namespace_loader_waits_for_service_not_scan_order(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "a_consumer",
    -        "from pydantic import BaseModel\n"
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'a_consumer'\n"
    -        "version = '1.0.0'\n"
    -        "VALUE = ServiceKey('fixture.value')\n"
    -        "inject = (VALUE,)\n"
    -        "observed = None\n"
    -        "disposed = False\n"
    -        "class Config(BaseModel):\n"
    -        "    suffix: str = 'default'\n"
    -        "async def apply(ctx, config):\n"
    -        "    global observed, disposed\n"
    -        "    observed = (ctx.require(VALUE), ctx.runtime.plugin_id, "
    -        "ctx.runtime.workspace.name, config.suffix)\n"
    -        "    def cleanup():\n"
    -        "        global disposed\n"
    -        "        disposed = True\n"
    -        "    await ctx.effect(lambda: cleanup, label='consumer')\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "z_provider",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'z_provider'\n"
    -        "version = '1.0.0'\n"
    -        "VALUE = ServiceKey('fixture.value')\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.provide(VALUE, 'ready')\n",
    -    )
    -    config_dir = tmp_path / "workspace" / "plugin-data" / "a_consumer-builtin"
    -    config_dir.mkdir(parents=True)
    -    (config_dir / "config.local.toml").write_text(
    -        "suffix = 'configured'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    consumer = manager.generation("a_consumer")
    -    snapshot = manager.current_snapshot
    -    assert consumer is not None and snapshot is not None
    -    assert isinstance(consumer.instance, ComposablePlugin)
    -    assert not hasattr(consumer.instance, "context")
    -    assert consumer.plugin_dir == tmp_path / "plugins" / "a_consumer"
    -    assert consumer.config.suffix == "configured"  # type: ignore[union-attr]
    -    assert consumer.instance.module.observed == (
    -        "ready",
    -        "a_consumer",
    -        "workspace",
    -        "configured",
    -    )
    -    assert snapshot.composition_root is not None
    -    assert snapshot.composition_topology is not None
    -    assert snapshot.composition_topology.services == (
    -        "core.commands",
    -        "fixture.value",
    -    )
    -    assert tuple(item.name for item in snapshot.composition_topology.fibers) == (
    -        "a_consumer",
    -        "z_provider",
    -    )
    -
    -    await manager.terminate_all()
    -
    -    assert consumer.instance.module.disposed is True
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_loader_publishes_declared_package_contributions(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "package_contributor",
    -        "api_version = 3\n"
    -        "name = 'package_contributor'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('skills',)\n"
    -        "drift_skill_roots = ('drift/skills',)\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "web_module = 'web_module.js'\n"
    -        "web_requires = ('web.root.v1',)\n"
    -        "web_provides = ()\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    skill_dir = plugin_dir / "skills" / "package-skill"
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\nname: package-skill\ndescription: package skill\n---\nnormal body\n",
    -        encoding="utf-8",
    -    )
    -    drift_skill_dir = plugin_dir / "drift" / "skills" / "package-drift"
    -    drift_skill_dir.mkdir(parents=True)
    -    (drift_skill_dir / "SKILL.md").write_text(
    -        "---\nname: package-drift\ndescription: package drift\n---\ndrift body\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        "from agent.plugin_composition import DashboardContext\n"
    -        "def plugin_enabled(context):\n"
    -        "    return isinstance(context, DashboardContext) and not context.validation\n"
    -        "def register(app, context):\n"
    -        "    assert not hasattr(app.state, 'memory_admin')\n"
    -        "    assert not hasattr(app.state, 'memory_store')\n"
    -        "    (context.data_root / 'dashboard-context-ready').write_text(context.plugin_id)\n"
    -        "    @app.get('/api/dashboard/package-contributor')\n"
    -        "    def status(): return {'plugin': 'package_contributor'}\n"
    -        "    class Closeable:\n"
    -        "        def __init__(self, path): self.path = path\n"
    -        "        def close(self): self.path.write_text('closed')\n"
    -        "    return (\n"
    -        "        Closeable(context.data_root / 'dashboard-close-one'),\n"
    -        "        Closeable(context.data_root / 'dashboard-close-two'),\n"
    -        "    )\n",
    -        encoding="utf-8",
    -    )
    -    web_source = (
    -        "import React from 'react';\n"
    -        "import { jsx } from 'react/jsx-runtime';\n"
    -        "import { createRoot } from 'react-dom/client';\n"
    -        "import { currentTheme } from '@akashic/web-ui-v1';\n"
    -        "const helper = () => null, T = (ctx) => {\n"
    -        "  const label = 'import a connection'; // import is ordinary copy\n"
    -        "  const marker = /import/;\n"
    -        "  if (ctx) /export function activate/.test(label);\n"
    -        "  const api = {import() {}}; api.import();\n"
    -        "  return ctx.ui.inject('web.root.v1', (mount) =>\n"
    -        "    mount.register({id: 'fixture', render() {}}));\n"
    -        "};\n"
    -        "export { T as activate };\n"
    -    )
    -    (plugin_dir / "web_module.js").write_text(web_source, encoding="utf-8")
    -    (plugin_dir / "web_module.css").write_text(".fixture { display: block; }\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    generation = manager.generation("package_contributor")
    -    snapshot = manager.current_snapshot
    -    assert generation is not None and snapshot is not None
    -    assert generation.contributions.skill_roots == ((plugin_dir / "skills").resolve(),)
    -    assert generation.contributions.drift_skill_roots == (
    -        (plugin_dir / "drift" / "skills").resolve(),
    -    )
    -    assert (
    -        generation.contributions.dashboard_module
    -        == (plugin_dir / "dashboard.py").resolve()
    -    )
    -    web_asset = generation.contributions.web_module
    -    assert web_asset is not None and web_asset.module == web_source
    -    assert web_asset.requires == ("web.root.v1",)
    -    assert web_asset.provides == ()
    -    assert len(web_asset.contract_sha256) == 64
    -    active = {item.plugin_id: item for item in manager.active_plugins()}
    -    assert active["package_contributor"].skill_roots == (
    -        (plugin_dir / "skills").resolve(),
    -    )
    -    assert active["package_contributor"].drift_skill_roots == (
    -        (plugin_dir / "drift" / "skills").resolve(),
    -    )
    -    catalog_id = snapshot.skill_catalog_generation_id
    -    assert catalog_id is not None
    -    catalog = manager._skill_host.get(catalog_id)
    -    assert catalog is not None
    -    assert snapshot.plugin_skill_index is not None
    -    assert snapshot.web_ui_catalog is not None
    -    assert [item.plugin_id for item in snapshot.web_ui_catalog.modules] == [
    -        "package_contributor"
    -    ]
    -    assert snapshot.web_ui_catalog.modules[0].asset is web_asset
    -    assert set(snapshot.plugin_skill_index.records) == {"package-skill"}
    -    assert set(catalog.drift.records) == {"package-drift"}
    -    assert snapshot.plugin_skill_index.records["package-skill"].root_dir != skill_dir
    -
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -    dashboard_host.prepare_snapshot(snapshot)
    -    assert len(snapshot.dashboard_bindings) == 1
    -    binding = snapshot.dashboard_bindings[0]
    -    assert isinstance(binding, DashboardBinding)
    -    assert binding.plugin_id == "package_contributor"
    -    assert binding.runtime_data_root == generation.data_dir.resolve()
    -    assert (generation.data_dir / "dashboard-context-ready").read_text() == (
    -        "package_contributor"
    -    )
    -    assert [route.path for route in binding.routes] == [
    -        "/api/dashboard/package-contributor"
    -    ]
    -
    -    await manager.terminate_all()
    -
    -    assert (generation.data_dir / "dashboard-close-one").is_file()
    -    assert (generation.data_dir / "dashboard-close-two").is_file()
    -
    -
    -@pytest.mark.asyncio
    -async def test_web_module_without_its_ui_provider_still_publishes(
    -    tmp_path: Path,
    -) -> None:
    -    """Keep runtime activation independent from a missing browser mount."""
    -
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "headless_capability",
    -        "api_version = 3\n"
    -        "name = 'headless_capability'\n"
    -        "version = '1.0.0'\n"
    -        "web_module = 'web_module.js'\n"
    -        "web_requires = ('optional.surface.v1',)\n"
    -        "async def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "web_module.js").write_text(
    -        "export function activate(ctx) {\n"
    -        "  return ctx.ui.inject('optional.surface.v1', () => () => {});\n"
    -        "}\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.web_ui_catalog is not None
    -    assert tuple(item.plugin_id for item in snapshot.web_ui_catalog.modules) == (
    -        "headless_capability",
    -    )
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_dashboard_rejects_legacy_register_signature(tmp_path: Path) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "legacy_dashboard_signature",
    -        "api_version = 3\n"
    -        "name = 'legacy_dashboard_signature'\n"
    -        "version = '1.0.0'\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        "def register(app, plugin_dir, workspace): return None\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -
    -    with pytest.raises(TypeError, match="missing 1 required positional argument"):
    -        dashboard_host.prepare_snapshot(snapshot)
    -
    -    assert snapshot.dashboard_bindings == ()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("dashboard_source", "message"),
    -    [
    -        (
    -            "plugin_enabled = 1\n" "def register(app, context): return None\n",
    -            "plugin_enabled 必须是可调用对象",
    -        ),
    -        (
    -            "async def plugin_enabled(context): return True\n"
    -            "def register(app, context): return None\n",
    -            "plugin_enabled 不支持 async",
    -        ),
    -        (
    -            "async def register(app, context): return None\n",
    -            "register 不支持 async",
    -        ),
    -        (
    -            "def register(app, context): return object()\n",
    -            "register 返回值不是 closeable",
    -        ),
    -    ],
    -)
    -async def test_v3_dashboard_rejects_invalid_callable_contracts(
    -    tmp_path: Path,
    -    dashboard_source: str,
    -    message: str,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "invalid_dashboard_contract",
    -        "api_version = 3\n"
    -        "name = 'invalid_dashboard_contract'\n"
    -        "version = '1.0.0'\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        dashboard_source,
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    generation = manager.generation("invalid_dashboard_contract")
    -    snapshot = manager.current_snapshot
    -    assert generation is not None and snapshot is not None
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -
    -    with pytest.raises(RuntimeError, match=message):
    -        dashboard_host.prepare_snapshot(snapshot)
    -
    -    assert snapshot.dashboard_bindings == ()
    -    assert tuple(generation.data_dir.iterdir()) == ()
    -    assert f"{generation.module_path}.dashboard" not in sys.modules
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.parametrize(
    -    ("declaration", "message"),
    -    [
    -        ("skill_roots = 'skills'", "skill_roots 必须是字符串序列"),
    -        ("drift_skill_roots = ('',)", "drift_skill_roots 必须只包含非空字符串"),
    -        ("skill_roots = ('skills', 'skills')", "skill_roots 不得重复"),
    -        (
    -            "workspace_roots = ('nested/root',)",
    -            "workspace_roots 必须是顶层目录名",
    -        ),
    -        (
    -            "workspace_roots = ('memes', 'memes')",
    -            "workspace_roots 不得重复",
    -        ),
    -        (
    -            "workspace_roots = ('plugin-data',)",
    -            "workspace_roots 不得声明 Core 保留目录 plugin-data",
    -        ),
    -        (
    -            "workspace_roots = ('runtime',)",
    -            "workspace_roots 不得声明 Core 保留目录 runtime",
    -        ),
    -        (
    -            "workspace_files = ('plugin-data/secret.db',)",
    -            "workspace_files 必须是 workspace 内相对文件路径",
    -        ),
    -        (
    -            "workspace_files = ('memory/../sessions.db',)",
    -            "workspace_files 必须是 workspace 内相对文件路径",
    -        ),
    -        ("dashboard_module = ''", "dashboard_module 必须是非空字符串或 None"),
    -        ("is_active = 1", "is_active 必须是可调用对象"),
    -    ],
    -)
    -def test_v3_namespace_rejects_invalid_package_contributions(
    -    declaration: str,
    -    message: str,
    -) -> None:
    -    from types import ModuleType
    -
    -    module = ModuleType("invalid_v3_contribution")
    -    module.api_version = 3
    -    module.name = "invalid"
    -    module.version = "1.0.0"
    -    module.apply = lambda ctx, config: None
    -    exec(declaration, module.__dict__)
    -
    -    with pytest.raises(ValueError, match=message):
    -        _ = ComposablePlugin.from_module(module)
    -
    -
    -def test_v3_namespace_freezes_package_contribution_lists() -> None:
    -    roots = ["skills"]
    -    module = ModuleType("frozen_v3_contribution")
    -    module.api_version = 3
    -    module.name = "frozen"
    -    module.version = "1.0.0"
    -    module.skill_roots = roots
    -    module.apply = lambda ctx, config: None
    -
    -    plugin = ComposablePlugin.from_module(module)
    -    roots.append("mutated")
    -
    -    assert plugin.skill_roots == ("skills",)
    -
    -
    -def test_v3_namespace_accepts_exact_nested_workspace_files() -> None:
    -    module = ModuleType("nested_workspace_file")
    -    module.api_version = 3
    -    module.name = "nested_workspace_file"
    -    module.version = "1.0.0"
    -    module.workspace_files = ("memory/MEMORY.md", "memory/SELF.md")
    -    module.apply = lambda ctx, config: None
    -
    -    plugin = ComposablePlugin.from_module(module)
    -
    -    assert plugin.workspace_files == ("memory/MEMORY.md", "memory/SELF.md")
    -
    -
    -@pytest.mark.parametrize(
    -    "declaration",
    -    [
    -        "def apply(ctx): pass",
    -        "def apply(): pass",
    -        "def apply(ctx, config, extra): pass",
    -        "def apply(ctx, config=None): pass",
    -        "def apply(*args): pass",
    -        "def apply(ctx, *, config): pass",
    -        "def apply(config, ctx): pass",
    -    ],
    -)
    -def test_v3_namespace_rejects_noncanonical_apply_signature(
    -    declaration: str,
    -) -> None:
    -    module = ModuleType("invalid_v3_apply")
    -    module.api_version = 3
    -    module.name = "invalid"
    -    module.version = "1.0.0"
    -    exec(declaration, module.__dict__)
    -
    -    with pytest.raises(
    -        ValueError,
    -        match=r"apply 必须精确声明 apply\(ctx, config\)",
    -    ):
    -        _ = ComposablePlugin.from_module(module)
    -
    -
    -@pytest.mark.parametrize(
    -    "declaration",
    -    [
    -        "def apply(ctx, config): pass",
    -        "async def apply(ctx, config): pass",
    -        "def apply(ctx, config, /): pass",
    -    ],
    -)
    -def test_v3_namespace_accepts_canonical_apply_signature(declaration: str) -> None:
    -    module = ModuleType("valid_v3_apply")
    -    module.api_version = 3
    -    module.name = "valid"
    -    module.version = "1.0.0"
    -    exec(declaration, module.__dict__)
    -
    -    plugin = ComposablePlugin.from_module(module)
    -
    -    assert plugin.name == "valid"
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_manager_rejects_invalid_apply_before_plugin_data_creation(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "invalid_apply",
    -        "api_version = 3\n"
    -        "name = 'invalid_apply'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.generation("invalid_apply") is None
    -    assert not (
    -        tmp_path / "workspace" / "plugin-data" / "invalid_apply-builtin"
    -    ).exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_manager_validates_plugin_data_path_before_config_read(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "invalid_apply",
    -        "api_version = 3\n"
    -        "name = 'invalid_apply'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx): pass\n",
    -    )
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    external = tmp_path / "external-plugin-data"
    -    external.mkdir()
    -    (workspace / "plugin-data").symlink_to(external, target_is_directory=True)
    -    config_revision_called = False
    -
    -    def unexpected_config_revision(_path: Path) -> str:
    -        nonlocal config_revision_called
    -        config_revision_called = True
    -        raise AssertionError("config revision must not cross the plugin-data boundary")
    -
    -    monkeypatch.setattr(
    -        plugin_manager_module,
    -        "_file_revision",
    -        unexpected_config_revision,
    -    )
    -    manager = _manager(tmp_path)
    -
    -    with pytest.raises(ValueError, match="插件数据目录不能穿过符号链接"):
    -        await manager.load_all()
    -
    -    assert config_revision_called is False
    -    assert tuple(external.iterdir()) == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_loader_rejects_workspace_root_that_is_not_directory(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "invalid_workspace_root",
    -        "api_version = 3\n"
    -        "name = 'invalid_workspace_root'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "memes").write_text("not a directory", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.generation("invalid_workspace_root") is None
    -    gate = manager.latest_gate("invalid_workspace_root")
    -    assert gate is not None
    -    assert gate.status == "failed"
    -    assert "workspace root 不是目录" in gate.failure_reason
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_loader_rejects_workspace_root_symlink_outside_workspace(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "escaped_workspace_root",
    -        "api_version = 3\n"
    -        "name = 'escaped_workspace_root'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    outside = tmp_path / "outside-memes"
    -    outside.mkdir()
    -    (workspace / "memes").symlink_to(outside, target_is_directory=True)
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.generation("escaped_workspace_root") is None
    -    gate = manager.latest_gate("escaped_workspace_root")
    -    assert gate is not None
    -    assert gate.status == "failed"
    -    assert "workspace root 不能是符号链接" in gate.failure_reason
    -    assert tuple(outside.iterdir()) == ()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_candidate_never_copies_workspace_root_symlink_target(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "candidate_workspace_root",
    -        "api_version = 3\n"
    -        "name = 'candidate_workspace_root'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -    workspace = tmp_path / "workspace"
    -    outside = tmp_path / "outside-candidate-memes"
    -    outside.mkdir()
    -    marker = outside / "must-not-copy.txt"
    -    marker.write_text("outside", encoding="utf-8")
    -    (workspace / "memes").symlink_to(outside, target_is_directory=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'candidate_workspace_root'\n"
    -        "version = '2.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -
    -    candidate = await manager.prepare_candidate("candidate_workspace_root")
    -
    -    assert candidate is None
    -    assert manager.current_snapshot is stable
    -    assert marker.read_text(encoding="utf-8") == "outside"
    -    assert not tuple((workspace / "plugin-data").glob("**/must-not-copy.txt"))
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_candidate_rejects_workspace_root_declaration_drift(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "workspace_root_drift",
    -        "api_version = 3\n"
    -        "name = 'workspace_root_drift'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -    original_clone = manager._clone_candidate_composable
    -
    -    def clone_with_drift(
    -        generation: PluginGeneration,
    -        *,
    -        candidate_owner: PluginGeneration,
    -        attempt_workspace: Path,
    -    ) -> tuple[ComposablePlugin, str, Path, object]:
    -        clone, module_path, data_dir, config = original_clone(
    -            generation,
    -            candidate_owner=candidate_owner,
    -            attempt_workspace=attempt_workspace,
    -        )
    -        clone.workspace_roots = ("drifted",)
    -        return clone, module_path, data_dir, config
    -
    -    monkeypatch.setattr(manager, "_clone_candidate_composable", clone_with_drift)
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'workspace_root_drift'\n"
    -        "version = '2.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -
    -    candidate = await manager.prepare_candidate("workspace_root_drift")
    -
    -    assert candidate is None
    -    assert manager.current_snapshot is stable
    -    gate = manager.latest_gate("workspace_root_drift")
    -    assert gate is not None
    -    assert "workspace_roots 与 generation 冻结声明不一致" in gate.failure_reason
    -    assert not any("__candidate_" in name for name in sys.modules)
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.parametrize("value", [None, 1, "active"])
    -def test_v3_active_predicate_must_return_bool(value: object) -> None:
    -    from types import ModuleType
    -
    -    module = ModuleType("invalid_v3_active_result")
    -    module.api_version = 3
    -    module.name = "invalid_active"
    -    module.version = "1.0.0"
    -    module.apply = lambda ctx, config: None
    -    module.is_active = lambda services: value
    -    plugin = ComposablePlugin.from_module(module)
    -
    -    with pytest.raises(RuntimeError, match="is_active 必须返回 bool"):
    -        plugin.bind_static_services(ServiceView.freeze({}))
    -
    -
    -def test_v3_active_predicate_rejects_async_without_leaking_coroutine() -> None:
    -    from types import ModuleType
    -
    -    module = ModuleType("async_v3_active_result")
    -    module.api_version = 3
    -    module.name = "async_active"
    -    module.version = "1.0.0"
    -    module.apply = lambda ctx, config: None
    -
    -    async def active(services: ServiceView) -> bool:
    -        return True
    -
    -    module.is_active = active
    -    plugin = ComposablePlugin.from_module(module)
    -
    -    with pytest.raises(RuntimeError, match="is_active 不支持 async"):
    -        plugin.bind_static_services(ServiceView.freeze({}))
    -
    -
    -@pytest.mark.asyncio
    -async def test_inactive_v3_does_not_wait_for_declared_runtime_dependency(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "inactive_missing",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'inactive_missing'\n"
    -        "version = '1.0.0'\n"
    -        "MISSING = ServiceKey('missing.runtime')\n"
    -        "inject = (MISSING,)\n"
    -        "def is_active(services): return False\n"
    -        "def apply(ctx, config): raise RuntimeError('inactive apply ran')\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.composition_root is not None
    -    assert snapshot.composition_root.receipt().ready is True
    -    assert snapshot.composition_root.receipt().required_pending == ()
    -    assert snapshot.composition_topology is not None
    -    fiber = next(
    -        item
    -        for item in snapshot.composition_topology.fibers
    -        if item.name == "inactive_missing"
    -    )
    -    assert fiber.dependencies == ("missing.runtime",)
    -    assert fiber.static_active is False
    -    assert snapshot.active_generations() == ()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_package_contribution_path_cannot_escape_plugin_root(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    outside = tmp_path / "plugins" / "outside"
    -    outside.mkdir(parents=True)
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "escaped_contributor",
    -        "api_version = 3\n"
    -        "name = 'escaped_contributor'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('../outside',)\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert "插件 能力目录 越界" in caplog.text
    -    assert manager.current_snapshot is None
    -    assert manager.generation("escaped_contributor") is None
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    "web_source",
    -    [
    -        "import React from './react.js';\nexport function activate() { return () => {}; }\n",
    -        "import React from 'preact';\nexport function activate() { return () => {}; }\n",
    -        "import { Button } from '@akashic/dashboard-ui';\n"
    -        "export function activate() { return () => {}; }\n",
    -        "import React from 'https://example.com/react.js';\n"
    -        "export function activate() { return () => {}; }\n",
    -        "export function activate() { import/**/('./late.js'); return () => {}; }\n",
    -        "export function activate() { `${import('./late.js')}`; return () => {}; }\n",
    -        "export { helper } from './helper.js';\nexport function activate() { return () => {}; }\n",
    -        "async function activate() { return () => {}; }\nexport { activate };\n",
    -        "export const activate = (async () => () => {});\n",
    -        "if (true) /export function activate/.test('copy');\n"
    -        "export const notActivate = () => {};\n",
    -    ],
    -)
    -async def test_v3_web_module_failure_never_publishes(
    -    tmp_path: Path,
    -    web_source: str,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "broken_web",
    -        "api_version = 3\n"
    -        "name = 'broken_web'\n"
    -        "version = '1.0.0'\n"
    -        "web_module = 'web_module.js'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "web_module.js").write_text(
    -        web_source,
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.generation("broken_web") is None
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    "stylesheet",
    -    [
    -        "@keyframes spin { to { transform: rotate(1turn); } }\n",
    -        "@keyframes broken_style-spin { to { transform: rotate(1turn); } }\n",
    -        "@font-face { font-family: shared; src: url(data:font/woff2;base64,AA); }\n",
    -    ],
    -)
    -async def test_v3_web_stylesheet_global_names_never_publish(
    -    tmp_path: Path,
    -    stylesheet: str,
    -) -> None:
    -    """Reject CSS names that @scope cannot isolate from sibling plugins."""
    -
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "broken_style",
    -        "api_version = 3\n"
    -        "name = 'broken_style'\n"
    -        "version = '1.0.0'\n"
    -        "web_module = 'web_module.js'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "web_module.js").write_text(
    -        "export function activate() { return () => {}; }\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "web_module.css").write_text(stylesheet, encoding="utf-8")
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.generation("broken_style") is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_package_contribution_rejects_duplicate_resolved_roots(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "duplicate_contributor",
    -        "api_version = 3\n"
    -        "name = 'duplicate_contributor'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('skills', './skills')\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "skills").mkdir()
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert "插件能力目录重复" in caplog.text
    -    assert manager.current_snapshot is None
    -    assert manager.generation("duplicate_contributor") is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_loader_fails_loud_when_required_service_never_appears(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "waiting",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'waiting'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (ServiceKey('never.provided'),)\n"
    -        "def apply(ctx, config):\n"
    -        "    raise AssertionError('pending plugin must not apply')\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    with pytest.raises(RuntimeError, match="never.provided"):
    -        await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.active_plugins() == []
    -    assert manager._snapshot_store.retained_snapshot_ids == ()
    -    assert manager._active_generations == {}
    -    assert manager._scopes == {}
    -    assert not (tmp_path / "workspace" / "plugin-data" / "waiting-builtin").exists()
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_stable_boot_publishes_one_complete_snapshot(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "independent",
    -        "api_version = 3\n"
    -        "name = 'independent'\n"
    -        "version = '1.0.0'\n"
    -        "activated = False\n"
    -        "def apply(ctx, config):\n"
    -        "    global activated\n"
    -        "    activated = True\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "consumer",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'consumer'\n"
    -        "version = '1.0.0'\n"
    -        "VALUE = ServiceKey('fixture.batch')\n"
    -        "inject = (VALUE,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    assert ctx.require(VALUE) == 'ready'\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "provider",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'provider'\n"
    -        "version = '1.0.0'\n"
    -        "VALUE = ServiceKey('fixture.batch')\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.provide(VALUE, 'ready')\n",
    -    )
    -    manager = _manager(tmp_path)
    -    installed: list[object] = []
    -    original_install = manager._snapshot_store.install
    -
    -    def record_install(snapshot: object) -> None:
    -        installed.append(snapshot)
    -        original_install(snapshot)  # type: ignore[arg-type]
    -
    -    manager._snapshot_store.install = record_install  # type: ignore[method-assign]
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    independent = manager.generation("independent")
    -    assert snapshot is not None and independent is not None
    -    assert len(installed) == 1
    -    assert set(snapshot.generations) == {"consumer", "independent", "provider"}
    -    assert snapshot.composition_root is not None
    -    assert snapshot.composition_topology is not None
    -    assert snapshot.composition_topology.services == (
    -        "core.commands",
    -        "fixture.batch",
    -    )
    -    assert independent.instance.module.activated is True
    -    catalog_id = snapshot.skill_catalog_generation_id
    -    assert catalog_id is not None
    -    assert manager._skill_host.get(catalog_id) is not None
    -
    -    await manager.terminate_all()
    -    assert manager._skill_host.get(catalog_id) is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_sealing_runs_once_after_all_services_are_ready(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "sealing_probe",
    -        "from agent.plugin_composition import SNAPSHOT_SEALING\n"
    -        "api_version = 3\n"
    -        "name = 'sealing_probe'\n"
    -        "version = '1.0.0'\n"
    -        "events = []\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.on(SNAPSHOT_SEALING, lambda _event: events.append('sealed'))\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    generation = manager.generation("sealing_probe")
    -    assert generation is not None
    -    assert generation.instance.module.events == ["sealed"]
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_snapshot_sealing_rejects_bail_before_publication(tmp_path: Path) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "sealing_bail",
    -        "from agent.plugin_composition import Bail, SNAPSHOT_SEALING\n"
    -        "api_version = 3\n"
    -        "name = 'sealing_bail'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.on(SNAPSHOT_SEALING, lambda _event: Bail('stop'))\n",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager.generation("sealing_bail") is None
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_rebuilds_complete_explicit_service_component(
    -    tmp_path: Path,
    -) -> None:
    -    plugins = tmp_path / "plugins"
    -    driver_dir = _write_plugin(
    -        plugins,
    -        "a_driver",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'a_driver'\n"
    -        "version = '1.0.0'\n"
    -        "DRIVERS = ServiceKey('fixture.drivers')\n"
    -        "inject = (DRIVERS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    assert ctx.require(DRIVERS) is not None\n",
    -    )
    -    _write_plugin(
    -        plugins,
    -        "b_models",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'b_models'\n"
    -        "version = '1.0.0'\n"
    -        "DRIVERS = ServiceKey('fixture.drivers')\n"
    -        "CHAT = ServiceKey('fixture.chat')\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.provide(DRIVERS, object())\n"
    -        "    await ctx.provide(CHAT, object())\n",
    -    )
    -    _write_plugin(
    -        plugins,
    -        "c_driver",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'c_driver'\n"
    -        "version = '1.0.0'\n"
    -        "DRIVERS = ServiceKey('fixture.drivers')\n"
    -        "inject = (DRIVERS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    assert ctx.require(DRIVERS) is not None\n",
    -    )
    -    _write_plugin(
    -        plugins,
    -        "d_consumer",
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'd_consumer'\n"
    -        "version = '1.0.0'\n"
    -        "CHAT = ServiceKey('fixture.chat')\n"
    -        "inject = (CHAT,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    assert ctx.require(CHAT) is not None\n",
    -    )
    -    _write_plugin(
    -        plugins,
    -        "unrelated",
    -        "api_version = 3\n"
    -        "name = 'unrelated'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    (driver_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import ServiceKey\n"
    -        "api_version = 3\n"
    -        "name = 'a_driver'\n"
    -        "version = '1.0.1'\n"
    -        "DRIVERS = ServiceKey('fixture.drivers')\n"
    -        "inject = (DRIVERS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    assert ctx.require(DRIVERS) is not None\n",
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("a_driver")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    root = candidate.runtime_snapshot.composition_root
    -
    -    assert isinstance(root, CompositionOverlay)
    -    assert root.replaced_plugin_ids == frozenset(
    -        {"a_driver", "b_models", "c_driver", "d_consumer"}
    -    )
    -    await manager.publish_prepared("a_driver")
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_cancelled_stable_batch_finishes_all_cleanup(tmp_path: Path) -> None:
    -    first_cleanup = tmp_path / "first-cleaned"
    -    blocking_started = tmp_path / "blocking-started"
    -    root_cleanup = tmp_path / "root-cleaned"
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "a_first",
    -        "from pathlib import Path\n"
    -        "api_version = 3\n"
    -        "name = 'a_first'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        f"    await ctx.effect(lambda: lambda: Path({str(first_cleanup)!r}).touch(), label='marker')\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "b_root",
    -        "from pathlib import Path\n"
    -        "api_version = 3\n"
    -        "name = 'b_root'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        f"    await ctx.effect(lambda: lambda: Path({str(root_cleanup)!r}).touch(), label='marker')\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "z_blocking",
    -        "import asyncio\n"
    -        "from pathlib import Path\n"
    -        "api_version = 3\n"
    -        "name = 'z_blocking'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        f"    Path({str(blocking_started)!r}).touch()\n"
    -        "    await asyncio.Event().wait()\n",
    -    )
    -    manager = _manager(tmp_path)
    -    original_discard = manager._discard_stable_batch
    -    discard_started = asyncio.Event()
    -
    -    async def delayed_discard(*args: object, **kwargs: object) -> None:
    -        discard_started.set()
    -        await asyncio.sleep(0.05)
    -        await original_discard(*args, **kwargs)  # type: ignore[arg-type]
    -
    -    manager._discard_stable_batch = delayed_discard  # type: ignore[method-assign]
    -    loading = asyncio.create_task(manager.load_all())
    -    while not blocking_started.exists():
    -        await asyncio.sleep(0)
    -
    -    loading.cancel()
    -    await discard_started.wait()
    -    loading.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await loading
    -
    -    assert first_cleanup.exists()
    -    assert root_cleanup.exists()
    -    assert manager.current_snapshot is None
    -    assert manager._snapshot_store.retained_snapshot_ids == ()
    -    assert manager._active_generations == {}
    -    assert manager._scopes == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_reload_keeps_old_root_until_snapshot_lease_drains(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "reloadable",
    -        "api_version = 3\n"
    -        "name = 'reloadable'\n"
    -        "version = '1.0.0'\n"
    -        "marker = 'old'\n"
    -        "disposed = False\n"
    -        "async def apply(ctx, config):\n"
    -        "    def cleanup():\n"
    -        "        global disposed\n"
    -        "        disposed = True\n"
    -        "    await ctx.effect(lambda: cleanup, label=marker)\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    old_generation = manager.generation("reloadable")
    -    old_snapshot = manager.current_snapshot
    -    assert old_generation is not None and old_snapshot is not None
    -    lease = manager._snapshot_store.lease()
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'reloadable'\n"
    -        "version = '1.0.0'\n"
    -        "marker = 'new'\n"
    -        "disposed = False\n"
    -        "async def apply(ctx, config):\n"
    -        "    def cleanup():\n"
    -        "        global disposed\n"
    -        "        disposed = True\n"
    -        "    await ctx.effect(lambda: cleanup, label=marker)\n",
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("reloadable")
    -    assert candidate is not None
    -
    -    publication = asyncio.create_task(manager.publish_prepared("reloadable"))
    -    while old_snapshot.accepting_leases:
    -        await asyncio.sleep(0)
    -    assert not publication.done()
    -    assert old_generation.instance.module.disposed is False
    -    await lease.release()
    -    result = await publication
    -
    -    assert result["publication_state"] == "committed"
    -    assert manager.current_snapshot is not old_snapshot
    -    active_root = manager.current_snapshot.composition_root
    -    assert active_root is not None
    -    active_runtime = active_root.root_fiber.children[0].runtime
    -    assert active_runtime is not None
    -    assert active_runtime.workspace == tmp_path / "workspace"
    -    assert candidate.validation_workspace is None
    -    assert old_generation.instance.module.disposed is True
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_direct_v3_rebuild_rejects_parent_ownership_drift(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "parent_drift",
    -        "api_version = 3\n"
    -        "name = 'parent_drift'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'parent_drift'\n"
    -        "version = '2.0.0'\n"
    -        "disposed = []\n"
    -        "async def apply(ctx, config):\n"
    -        "    validation = 'plugin-validation' in str(ctx.runtime.workspace)\n"
    -        "    async def apply_group(group_ctx):\n"
    -        "        if validation:\n"
    -        "            await group_ctx.mount(lambda _: None, name='worker')\n"
    -        "    await ctx.mount(apply_group, name='group')\n"
    -        "    if not validation:\n"
    -        "        await ctx.mount(lambda _: None, name='worker')\n"
    -        "    role = 'candidate' if validation else 'formal'\n"
    -        "    def cleanup():\n"
    -        "        disposed.append(role)\n"
    -        "    await ctx.effect(lambda: cleanup, label='parent-drift')\n",
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("parent_drift")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_view = candidate_root.topology_view()
    -    assert tuple((item.name, item.parent) for item in candidate_view.fibers) == (
    -        ("group", "parent_drift"),
    -        ("parent_drift", None),
    -        ("worker", "group"),
    -    )
    -    attempt_workspace = candidate_root.root_fiber.children[0].runtime
    -    assert attempt_workspace is not None
    -    attempt_root = attempt_workspace.workspace.parent
    -    clone_modules = {
    -        module_name
    -        for module_name in sys.modules
    -        if module_name.startswith(f"{candidate.module_path}__candidate_")
    -    }
    -    assert clone_modules
    -
    -    with pytest.raises(RuntimeError, match="snapshot identity 发生变化"):
    -        await manager.publish_prepared("parent_drift")
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.prepared_generation("parent_drift") is None
    -    assert candidate.scope.closed is True
    -    assert candidate.instance.module.disposed == ["formal"]
    -    assert clone_modules.isdisjoint(sys.modules)
    -    assert not attempt_root.exists()
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_direct_v3_invariant_failure_never_applies_to_formal_data(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "isolated_reload",
    -        "api_version = 3\n"
    -        "name = 'isolated_reload'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    pass\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None
    -
    -    (plugin_dir / "plugin.py").write_text(
    -        "from pathlib import Path\n"
    -        "api_version = 3\n"
    -        "name = 'isolated_reload'\n"
    -        "version = '2.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    Path(ctx.data_root, 'apply-probe').write_text('candidate')\n",
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("isolated_reload")
    -    assert candidate is not None and candidate.validation_workspace is not None
    -    validation_root = candidate.validation_workspace.parent
    -    candidate_snapshot = candidate.runtime_snapshot
    -    assert candidate_snapshot is not None
    -    candidate_root = candidate_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_runtime = candidate_root.root_fiber.children[0].runtime
    -    assert candidate_runtime is not None
    -    clone_modules = {
    -        module_name
    -        for module_name in sys.modules
    -        if module_name.startswith(f"{candidate.module_path}__candidate_")
    -    }
    -    assert clone_modules
    -    assert (candidate_runtime.data_dir / "apply-probe").is_file()
    -    first_attempt_root = candidate_runtime.workspace.parent
    -
    -    original_invariants = manager._post_publish_invariants
    -
    -    async def fail_invariant(*_args: object) -> None:
    -        raise RuntimeError("candidate invariant failed")
    -
    -    monkeypatch.setattr(manager, "_post_publish_invariants", fail_invariant)
    -    with pytest.raises(RuntimeError, match="candidate invariant failed"):
    -        await manager.publish_prepared("isolated_reload")
    -
    -    formal_probe = (
    -        tmp_path
    -        / "workspace"
    -        / "plugin-data"
    -        / "isolated_reload-builtin"
    -        / "apply-probe"
    -    )
    -    assert not formal_probe.exists()
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.prepared_generation("isolated_reload") is None
    -    assert candidate.scope.closed is True
    -    assert clone_modules.isdisjoint(sys.modules)
    -    assert not validation_root.exists()
    -    assert not first_attempt_root.exists()
    -
    -    monkeypatch.setattr(manager, "_post_publish_invariants", original_invariants)
    -    second = await manager.prepare_candidate("isolated_reload")
    -    assert second is not None and second.runtime_snapshot is not None
    -    second_root = second.runtime_snapshot.composition_root
    -    assert second_root is not None
    -    second_runtime = second_root.root_fiber.children[0].runtime
    -    assert second_runtime is not None
    -    second_attempt_root = second_runtime.workspace.parent
    -    second_clone_modules = {
    -        module_name
    -        for module_name in sys.modules
    -        if module_name.startswith(f"{second.module_path}__candidate_")
    -    }
    -    assert second_clone_modules
    -
    -    published = await manager.publish_prepared("isolated_reload")
    -
    -    assert published["publication_state"] == "committed"
    -    assert formal_probe.read_text(encoding="utf-8") == "candidate"
    -    assert second_clone_modules.isdisjoint(sys.modules)
    -    assert not second_attempt_root.exists()
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_cancelled_candidate_mount_cleans_partial_clones_and_data(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "a_first",
    -        "api_version = 3\n"
    -        "name = 'a_first'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.effect(lambda: None, label='first')\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "z_blocker",
    -        "import asyncio\n"
    -        "api_version = 3\n"
    -        "name = 'z_blocker'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    if 'plugin-validation' not in str(ctx.runtime.workspace):\n"
    -        "        return\n"
    -        "    (ctx.runtime.workspace / 'blocker-entered').write_text('ready')\n"
    -        "    await asyncio.Event().wait()\n",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None
    -    stable_root = stable_snapshot.composition_root
    -    validation_base = tmp_path / "workspace" / "runtime" / "plugin-validation"
    -
    -    preparing = asyncio.create_task(manager.prepare_candidate("z_blocker"))
    -    marker: Path | None = None
    -    for _ in range(200):
    -        markers = list(validation_base.rglob("blocker-entered"))
    -        if markers:
    -            marker = markers[0]
    -            break
    -        await asyncio.sleep(0.01)
    -    if marker is None:
    -        preparing.cancel()
    -        with pytest.raises(asyncio.CancelledError):
    -            await preparing
    -        pytest.fail("candidate Fiber did not enter apply")
    -
    -    attempt_root = marker.parent.parent
    -    clone_modules = {
    -        module_name for module_name in sys.modules if "__candidate_" in module_name
    -    }
    -    assert len(clone_modules) == 1
    -
    -    preparing.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await preparing
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.current_snapshot.composition_root is stable_root
    -    assert manager.prepared_generation("z_blocker") is None
    -    assert clone_modules.isdisjoint(sys.modules)
    -    assert not attempt_root.exists()
    -    assert not validation_base.exists() or not any(validation_base.iterdir())
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_v3_candidate_rebuilds_runtime_then_promotes(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "installed_v3"
    -    stable_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    latest_root = plugin_base / ".artifacts" / "2.0.0-bbbb"
    -    stable_root.mkdir(parents=True)
    -    latest_root.mkdir(parents=True)
    -    source = (
    -        "from pydantic import BaseModel\n"
    -        "from agent.plugin_composition import BACKGROUND_JOBS\n"
    -        "api_version = 3\n"
    -        "name = 'installed_v3'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('skills',)\n"
    -        "drift_skill_roots = ('drift/skills',)\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "inject = (BACKGROUND_JOBS,)\n"
    -        "class Config(BaseModel):\n"
    -        "    marker: str = 'default'\n"
    -        "applied = []\n"
    -        "disposed = []\n"
    -        "async def apply(ctx, config):\n"
    -        "    workspace = str(ctx.runtime.workspace)\n"
    -        "    writer = ctx.runtime.workspace / '.installed-v3-writer'\n"
    -        "    if writer.exists():\n"
    -        "        raise RuntimeError('installed writer already mounted')\n"
    -        "    writer.write_text(ctx.runtime.generation_id, encoding='utf-8')\n"
    -        "    applied.append((workspace, config.marker))\n"
    -        "    def cleanup():\n"
    -        "        disposed.append(workspace)\n"
    -        "        writer.unlink()\n"
    -        "    await ctx.effect(lambda: cleanup, label='runtime')\n"
    -    )
    -    (stable_root / "plugin.py").write_text(source, encoding="utf-8")
    -    (latest_root / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(stable_root, "installed_v3", "1.0.0")
    -    _write_static_v3_manifest(latest_root, "installed_v3", "2.0.0")
    -    for root, version in ((stable_root, "v1"), (latest_root, "v2")):
    -        skill_dir = root / "skills" / "installed-skill"
    -        skill_dir.mkdir(parents=True)
    -        (skill_dir / "SKILL.md").write_text(
    -            f"---\ndescription: installed {version}\n---\nbody {version}\n",
    -            encoding="utf-8",
    -        )
    -        drift_dir = root / "drift" / "skills" / "installed-drift"
    -        drift_dir.mkdir(parents=True)
    -        (drift_dir / "SKILL.md").write_text(
    -            f"---\ndescription: drift {version}\n---\ndrift {version}\n",
    -            encoding="utf-8",
    -        )
    -        (root / "dashboard.py").write_text(
    -            "def register(app, context): return None\n",
    -            encoding="utf-8",
    -        )
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-aaaa")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-bbbb")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"installed_v3@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    config_dir = tmp_path / "workspace" / "plugin-data" / "installed_v3-lab"
    -    config_dir.mkdir(parents=True)
    -    (config_dir / "config.local.toml").write_text(
    -        "marker = 'configured'\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    manager.bind_activity_host(ActivityHost(()))
    -    await manager.load_all()
    -    stable = manager.generation("installed_v3@lab")
    -    stable_snapshot = manager.current_snapshot
    -    assert stable is not None and stable_snapshot is not None
    -    assert stable_snapshot.plugin_skill_index is not None
    -    assert (
    -        "body v1" in stable_snapshot.plugin_skill_index.get("installed-skill").content
    -    )  # type: ignore[union-attr]
    -    stable_lease = manager.snapshot_store.lease()
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    result = (await manager.reconcile_changed())[0]
    -    candidate = manager.ready_candidate
    -
    -    assert result["publication_state"] == "latest_ready"
    -    assert candidate is not None
    -    assert not hasattr(candidate.instance, "context")
    -    assert candidate.plugin_dir == latest_root
    -    assert candidate.config.marker == "configured"  # type: ignore[union-attr]
    -    candidate_snapshot = candidate.runtime_snapshot
    -    assert candidate_snapshot is not None
    -    assert candidate_snapshot.plugin_skill_index is not None
    -    assert (
    -        "body v2"
    -        in candidate_snapshot.plugin_skill_index.get("installed-skill").content
    -    )  # type: ignore[union-attr]
    -    assert (
    -        candidate.contributions.dashboard_module
    -        == (latest_root / "dashboard.py").resolve()
    -    )
    -    candidate_root = candidate_snapshot.composition_root
    -    stable_root_runtime = manager.current_snapshot.composition_root
    -    assert candidate_root is not None
    -    assert candidate_root is not stable_root_runtime
    -    candidate_runtime = candidate_root.root_fiber.children[0].runtime
    -    assert candidate_runtime is not None
    -    assert "plugin-validation" in str(candidate_runtime.workspace)
    -    assert candidate_runtime.config.marker == "configured"  # type: ignore[union-attr]
    -    assert candidate.validation_workspace is not None
    -    validation_root = candidate.validation_workspace.parent
    -    clone_modules = {
    -        module_name
    -        for module_name in sys.modules
    -        if module_name.startswith(f"{candidate.module_path}__candidate_")
    -    }
    -    assert clone_modules
    -    original_start_runtime = manager._start_runtime_snapshot
    -    formal_started_after_pointer = False
    -
    -    async def observe_formal_start(snapshot) -> None:
    -        nonlocal formal_started_after_pointer
    -        if snapshot is not stable_snapshot:
    -            assert read_pointer(plugin_base, "stable") == latest_pointer
    -            assert read_pointer(plugin_base, "latest") == latest_pointer
    -            assert snapshot is manager.current_snapshot
    -            assert snapshot.accepting_leases
    -            formal_started_after_pointer = True
    -        await original_start_runtime(snapshot)
    -
    -    manager._start_runtime_snapshot = observe_formal_start  # type: ignore[method-assign]
    -    promotion = asyncio.create_task(manager.switch_ready("installed_v3@lab"))
    -    while stable_snapshot.accepting_leases:
    -        await asyncio.sleep(0)
    -    assert not promotion.done()
    -    assert stable.instance.module.disposed == []
    -    await stable_lease.release()
    -    promoted = await promotion
    -    assert formal_started_after_pointer
    -
    -    assert promoted["publication_state"] == "promoted"
    -    promoted_snapshot = manager.current_snapshot
    -    assert promoted_snapshot is not None
    -    assert promoted_snapshot.composition_root is not None
    -    assert promoted_snapshot.background_job_catalog is not None
    -    assert (
    -        promoted_snapshot.background_job_catalog.root_instance_token
    -        is promoted_snapshot.composition_root.instance_token
    -    )
    -    assert promoted_snapshot.plugin_skill_index is not None
    -    assert (
    -        "body v2" in promoted_snapshot.plugin_skill_index.get("installed-skill").content
    -    )  # type: ignore[union-attr]
    -    promoted_catalog_id = promoted_snapshot.skill_catalog_generation_id
    -    assert promoted_catalog_id is not None
    -    promoted_catalog = manager._skill_host.get(promoted_catalog_id)
    -    assert promoted_catalog is not None
    -    assert (
    -        "drift v2" in promoted_catalog.drift.get("installed-drift").content
    -    )  # type: ignore[union-attr]
    -    assert (
    -        promoted_snapshot.generations["installed_v3@lab"].contributions.dashboard_module
    -        == (latest_root / "dashboard.py").resolve()
    -    )
    -    assert candidate.instance.module.applied[-1] == (
    -        str(tmp_path / "workspace"),
    -        "configured",
    -    )
    -    assert clone_modules.isdisjoint(sys.modules)
    -    assert not validation_root.exists()
    -    assert stable.instance.module.disposed == [str(tmp_path / "workspace")]
    -    assert (tmp_path / "workspace" / ".installed-v3-writer").exists()
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_v3_dashboard_uses_composition_runtime_until_promotion(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "dashboard_v3"
    -    stable_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    latest_root = plugin_base / ".artifacts" / "2.0.0-bbbb"
    -    stable_root.mkdir(parents=True)
    -    latest_root.mkdir(parents=True)
    -    source = (
    -        "api_version = 3\n"
    -        "name = 'dashboard_v3'\n"
    -        "version = '1.0.0'\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "drift_skill_roots = ('drift/skills',)\n"
    -        "workspace_roots = ('memes',)\n"
    -        "def is_active(services): return True\n"
    -        "observed_workspace_root = None\n"
    -        "def apply(ctx, config):\n"
    -        "    global observed_workspace_root\n"
    -        "    observed_workspace_root = ctx.workspace_root('memes')\n"
    -    )
    -    (stable_root / "plugin.py").write_text(source, encoding="utf-8")
    -    (latest_root / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(stable_root, "dashboard_v3", "1.0.0")
    -    _write_static_v3_manifest(latest_root, "dashboard_v3", "2.0.0")
    -    (stable_root / "dashboard.py").write_text(
    -        "def register(app, context):\n"
    -        "    assert context.workspace_root('memes').is_dir()\n",
    -        encoding="utf-8",
    -    )
    -    (latest_root / "dashboard.py").write_text(
    -        "def register(app, context):\n"
    -        "    marker = 'candidate-registered' if context.validation else 'formal-registered'\n"
    -        "    (context.data_root / marker).write_text('ready')\n"
    -        "    shared = context.workspace_root('memes')\n"
    -        "    shared_marker = 'candidate-shared' if context.validation else 'formal-shared'\n"
    -        "    (shared / shared_marker).write_text('ready')\n"
    -        "    class Closeable:\n"
    -        "        def close(self):\n"
    -        "            (context.data_root / 'dashboard-v3-closed').write_text('closed')\n"
    -        "    return Closeable()\n",
    -        encoding="utf-8",
    -    )
    -    for artifact in (stable_root, latest_root):
    -        skill = artifact / "drift" / "skills" / "dashboard-v3-static"
    -        skill.mkdir(parents=True)
    -        (skill / "SKILL.md").write_text("# static projection\n", encoding="utf-8")
    -    formal_memes = tmp_path / "workspace" / "memes"
    -    formal_memes.mkdir(parents=True)
    -    (formal_memes / "manifest.json").write_text("{}\n", encoding="utf-8")
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-aaaa")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-bbbb")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"dashboard_v3@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    PluginSkillLinker(
    -        workspace=tmp_path / "workspace",
    -        plugin_roots=manager.skill_projection_roots,
    -    ).sync(manager.active_plugins())
    -    skill_link = tmp_path / "workspace" / "drift" / "skills" / "dashboard-v3-static"
    -    assert skill_link.exists()
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -    dashboard_host.prepare_initial_snapshot(stable_snapshot)
    -    manager.bind_dashboard_preparer(
    -        dashboard_host.prepare_snapshot,
    -        validation_releaser=dashboard_host.release_validation,
    -    )
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    first_change = (await manager.reconcile_changed())[0]
    -    first_gate = manager.latest_gate("dashboard_v3@lab")
    -    assert first_change["publication_state"] == "latest_ready", (
    -        first_change,
    -        None if first_gate is None else first_gate.failure_reason,
    -    )
    -    candidate = manager.ready_candidate
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    assert "dashboard_v3@lab" in {
    -        item.plugin_id for item in candidate.runtime_snapshot.active_generations()
    -    }
    -    validation_workspace = candidate.validation_workspace
    -    assert validation_workspace is not None
    -    candidate_binding = candidate.runtime_snapshot.dashboard_bindings[0]
    -    assert isinstance(candidate_binding, DashboardBinding)
    -    assert candidate_binding.validation is True
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    candidate_runtime = candidate_root.plugin_runtime("dashboard_v3@lab")
    -    assert candidate_binding.runtime_workspace == candidate_runtime.workspace.resolve()
    -    candidate_data_root = candidate_binding.runtime_data_root
    -    assert candidate_data_root is not None
    -    assert candidate_data_root == candidate_runtime.data_dir.resolve()
    -    assert candidate_data_root.is_relative_to(validation_workspace.parent)
    -    assert (candidate_data_root / "candidate-registered").is_file()
    -    candidate_memes = candidate_runtime.workspace_root("memes")
    -    assert candidate_memes != formal_memes.resolve()
    -    assert (candidate_memes / "manifest.json").read_text() == "{}\n"
    -    assert (candidate_memes / "candidate-shared").is_file()
    -    assert not (formal_memes / "candidate-shared").exists()
    -    assert not (candidate_data_root / "formal-registered").exists()
    -    production_data_root = tmp_path / "workspace" / "plugin-data" / "dashboard_v3-lab"
    -    assert not (production_data_root / "candidate-registered").exists()
    -    assert not (production_data_root / "formal-registered").exists()
    -    assert not (formal_memes / "candidate-shared").exists()
    -    validation_root = validation_workspace.parent
    -    validation_module = candidate_binding.module_name
    -
    -    await manager.drop_candidate("dashboard_v3@lab")
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert not validation_root.exists()
    -    assert validation_module not in sys.modules
    -    assert not (production_data_root / "candidate-registered").exists()
    -    assert not (production_data_root / "formal-registered").exists()
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    assert (await manager.reconcile_changed())[0]["publication_state"] == "latest_ready"
    -    promoted_candidate = manager.ready_candidate
    -    assert promoted_candidate is not None
    -    promoted_validation_workspace = promoted_candidate.validation_workspace
    -    assert promoted_validation_workspace is not None
    -    promoted_validation_root = promoted_validation_workspace.parent
    -
    -    promoted = await manager.switch_ready("dashboard_v3@lab")
    -
    -    assert promoted["publication_state"] == "promoted"
    -    current = manager.current_snapshot
    -    assert current is not None
    -    formal_binding = current.dashboard_bindings[0]
    -    assert isinstance(formal_binding, DashboardBinding)
    -    assert formal_binding.validation is False
    -    assert formal_binding.runtime_workspace == (tmp_path / "workspace").resolve()
    -    assert formal_binding.runtime_data_root == production_data_root.resolve()
    -    assert (production_data_root / "formal-registered").is_file()
    -    assert (formal_memes / "formal-shared").is_file()
    -    assert not (formal_memes / "candidate-shared").exists()
    -    assert not (production_data_root / "candidate-registered").exists()
    -    assert skill_link.exists()
    -    assert not promoted_validation_root.exists()
    -    promoted_generation = manager.generation("dashboard_v3@lab")
    -    assert promoted_generation is not None
    -    assert promoted_generation.instance.module.observed_workspace_root == (
    -        formal_memes.resolve()
    -    )
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_v3_dashboard_uses_exact_workspace_declarations(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "exact_workspace_root",
    -        "api_version = 3\n"
    -        "name = 'exact_workspace_root'\n"
    -        "version = '1.0.0'\n"
    -        "workspace_roots = ('memes',)\n"
    -        "workspace_files = ('sessions.db',)\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        "def register(app, context):\n"
    -        "    assert context.workspace_root('memes').name == 'memes'\n"
    -        "    assert context.workspace_file('sessions.db').name == 'sessions.db'\n",
    -        encoding="utf-8",
    -    )
    -    memes = tmp_path / "workspace" / "memes"
    -    memes.mkdir(parents=True)
    -    (tmp_path / "workspace" / "sessions.db").touch()
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    generation = manager.generation("exact_workspace_root")
    -    snapshot = manager.current_snapshot
    -    assert generation is not None and snapshot is not None
    -    generation.instance.workspace_roots = ("drifted",)
    -    generation.instance.workspace_files = ("drifted.db",)
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -
    -    dashboard_host.prepare_snapshot(snapshot)
    -
    -    assert len(snapshot.dashboard_bindings) == 1
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_inactive_v3_does_not_claim_active_plugin_skill_name(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_root = tmp_path / "plugins"
    -    for name, active in (("inactive_owner", False), ("active_owner", True)):
    -        plugin = _write_plugin(
    -            plugin_root,
    -            name,
    -            "api_version = 3\n"
    -            f"name = '{name}'\n"
    -            "version = '1.0.0'\n"
    -            "drift_skill_roots = ('drift/skills',)\n"
    -            f"def is_active(services): return {active!r}\n"
    -            "def apply(ctx, config): pass\n",
    -        )
    -        skill = plugin / "drift" / "skills" / "shared-static-skill"
    -        skill.mkdir(parents=True)
    -        (skill / "SKILL.md").write_text(f"# {name}\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -    PluginSkillLinker(
    -        workspace=tmp_path / "workspace",
    -        plugin_roots=manager.skill_projection_roots,
    -    ).sync(manager.active_plugins())
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert {item.plugin_id for item in snapshot.active_generations()} == {
    -        "active_owner"
    -    }
    -    link = tmp_path / "workspace" / "drift" / "skills" / "shared-static-skill"
    -    assert (
    -        link.resolve()
    -        == (
    -            plugin_root / "active_owner" / "drift" / "skills" / "shared-static-skill"
    -        ).resolve()
    -    )
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_builtin_v3_dashboard_candidate_clones_data_root_before_publish(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(
    -        tmp_path / "plugins",
    -        "dashboard_builtin_v3",
    -        "api_version = 3\n"
    -        "name = 'dashboard_builtin_v3'\n"
    -        "version = '1.0.0'\n"
    -        "from agent.plugin_composition import BACKGROUND_JOBS\n"
    -        "inject = (BACKGROUND_JOBS,)\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "def apply(ctx, config): pass\n",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        "def register(app, context): return None\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -    manager.bind_activity_host(ActivityHost(()))
    -    await manager.load_all()
    -    stable = manager.generation("dashboard_builtin_v3")
    -    stable_snapshot = manager.current_snapshot
    -    assert stable is not None and stable_snapshot is not None
    -    (stable.data_dir / "existing.txt").write_text("stable", encoding="utf-8")
    -    dashboard_host = PluginDashboardHost(
    -        core_routes=(),
    -    )
    -    dashboard_host.prepare_initial_snapshot(stable_snapshot)
    -    manager.bind_dashboard_preparer(
    -        dashboard_host.prepare_snapshot,
    -        validation_releaser=dashboard_host.release_validation,
    -    )
    -    (plugin_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'dashboard_builtin_v3'\n"
    -        "version = '2.0.0'\n"
    -        "from agent.plugin_composition import BACKGROUND_JOBS\n"
    -        "inject = (BACKGROUND_JOBS,)\n"
    -        "dashboard_module = 'dashboard.py'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "dashboard.py").write_text(
    -        "def register(app, context):\n"
    -        "    assert (context.data_root / 'existing.txt').read_text() == 'stable'\n"
    -        "    marker = 'candidate.txt' if context.validation else 'formal.txt'\n"
    -        "    (context.data_root / marker).write_text('ready')\n",
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("dashboard_builtin_v3")
    -    assert candidate is not None and candidate.validation_workspace is not None
    -    validation_root = candidate.validation_workspace.parent
    -
    -    result = await manager.publish_prepared("dashboard_builtin_v3")
    -
    -    assert result["publication_state"] == "committed"
    -    current = manager.current_snapshot
    -    assert current is not None
    -    assert current.composition_root is not None
    -    assert current.background_job_catalog is not None
    -    assert (
    -        current.background_job_catalog.root_instance_token
    -        is current.composition_root.instance_token
    -    )
    -    binding = current.dashboard_bindings[0]
    -    assert isinstance(binding, DashboardBinding)
    -    assert binding.runtime_data_root == stable.data_dir.resolve()
    -    assert (stable.data_dir / "existing.txt").read_text() == "stable"
    -    assert (stable.data_dir / "formal.txt").is_file()
    -    assert not (stable.data_dir / "candidate.txt").exists()
    -    assert not validation_root.exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_v3_candidate_health_blocks_promotion_until_recovered(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "installed_v3"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-bbbb"
    -    stable_artifact.mkdir(parents=True)
    -    latest_artifact.mkdir(parents=True)
    -    source = (
    -        "api_version = 3\n"
    -        "name = 'installed_v3'\n"
    -        "version = '1.0.0'\n"
    -        "health = None\n"
    -        "async def apply(ctx, config):\n"
    -        "    global health\n"
    -        "    health = await ctx.health('worker', required=True)\n"
    -    )
    -    (stable_artifact / "plugin.py").write_text(source, encoding="utf-8")
    -    (latest_artifact / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(stable_artifact, "installed_v3", "1.0.0")
    -    _write_static_v3_manifest(latest_artifact, "installed_v3", "2.0.0")
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-aaaa")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-bbbb")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"installed_v3@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    assert (await manager.reconcile_changed())[0]["publication_state"] == "latest_ready"
    -    candidate = manager.ready_candidate
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    clone_name = next(
    -        name
    -        for name in sys.modules
    -        if name.startswith(f"{candidate.module_path}__candidate_")
    -    )
    -    candidate_health = sys.modules[clone_name].health
    -    candidate_health.degrade("validation worker unavailable")
    -
    -    with pytest.raises(RuntimeError, match="required_degraded"):
    -        await manager.switch_ready("installed_v3@lab")
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.ready_candidate is candidate
    -    assert candidate_root.root_fiber.children[0].state.value == "active"
    -
    -    candidate_health.recover()
    -    promoted = await manager.switch_ready("installed_v3@lab")
    -
    -    assert promoted["publication_state"] == "promoted"
    -    assert manager.ready_candidate is None
    -    assert clone_name not in sys.modules
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_v3_candidate_incident_overflow_blocks_promotion(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "installed_v3"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-bbbb"
    -    stable_artifact.mkdir(parents=True)
    -    latest_artifact.mkdir(parents=True)
    -    source = (
    -        "api_version = 3\n"
    -        "name = 'installed_v3'\n"
    -        "version = '1.0.0'\n"
    -        "saved_ctx = None\n"
    -        "async def apply(ctx, config):\n"
    -        "    global saved_ctx\n"
    -        "    saved_ctx = ctx\n"
    -    )
    -    (stable_artifact / "plugin.py").write_text(source, encoding="utf-8")
    -    (latest_artifact / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(stable_artifact, "installed_v3", "1.0.0")
    -    _write_static_v3_manifest(latest_artifact, "installed_v3", "2.0.0")
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-aaaa")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-bbbb")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"installed_v3@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable_snapshot = manager.current_snapshot
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    assert (await manager.reconcile_changed())[0]["publication_state"] == "latest_ready"
    -    candidate = manager.ready_candidate
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    clone_name = next(
    -        name
    -        for name in sys.modules
    -        if name.startswith(f"{candidate.module_path}__candidate_")
    -    )
    -    candidate_context = sys.modules[clone_name].saved_ctx
    -    for index in range(1025):
    -        candidate_context.report_incident("probe", f"failure {index}")
    -
    -    with pytest.raises(RuntimeError, match="incident_overflowed"):
    -        await manager.switch_ready("installed_v3@lab")
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.ready_candidate is candidate
    -    dropped = await manager.drop_candidate("installed_v3@lab")
    -    assert dropped["publication_state"] == "discarded"
    -    assert clone_name not in sys.modules
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("owner_commit_fails", [False, True])
    -async def test_installed_v3_isolated_handoff_success_and_owner_failure(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    owner_commit_fails: bool,
    -) -> None:
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "installed_v3"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-bbbb"
    -    stable_artifact.mkdir(parents=True)
    -    latest_artifact.mkdir(parents=True)
    -    source = (
    -        "api_version = 3\n"
    -        "name = 'installed_v3'\n"
    -        "version = '1.0.0'\n"
    -        "from agent.plugin_composition import RUNTIME_STARTED, RUNTIME_STOPPING\n"
    -        "disposed = False\n"
    -        "started_roots = []\n"
    -        "stopped_roots = []\n"
    -        "async def apply(ctx, config):\n"
    -            "    global disposed\n"
    -            "    disposed = False\n"
    -            "    token = id(ctx._root_instance_token())\n"
    -            "    async def started(_):\n"
    -            "        async with ctx.runtime_scope():\n"
    -            "            started_roots.append(token)\n"
    -            "    await ctx.on(RUNTIME_STARTED, started)\n"
    -        "    await ctx.on(RUNTIME_STOPPING, lambda _: stopped_roots.append(token))\n"
    -        "    def cleanup():\n"
    -        "        global disposed\n"
    -        "        disposed = True\n"
    -        "    await ctx.effect(lambda: cleanup, label='runtime')\n"
    -    )
    -    (stable_artifact / "plugin.py").write_text(source, encoding="utf-8")
    -    (latest_artifact / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    _write_static_v3_manifest(stable_artifact, "installed_v3", "1.0.0")
    -    _write_static_v3_manifest(latest_artifact, "installed_v3", "2.0.0")
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-aaaa")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-bbbb")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"installed_v3@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.generation("installed_v3@lab")
    -    stable_snapshot = manager.current_snapshot
    -    assert stable is not None and stable_snapshot is not None
    -    old_root = stable_snapshot.composition_root
    -    assert old_root is not None
    -    runtime_services = asyncio.create_task(manager.run_runtime_services())
    -    while old_root.instance_token not in manager._runtime_started_roots:
    -        await asyncio.sleep(0)
    -
    -    write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -    assert (await manager.reconcile_changed())[0]["publication_state"] == "latest_ready"
    -    candidate = manager.ready_candidate
    -    assert candidate is not None and candidate.reload_tx_id is not None
    -    original_activate = manager._activate_published_generation
    -
    -    if owner_commit_fails:
    -        original_recover = manager._recover_stable_root
    -        original_write_pointers = plugin_manager_module.write_pointers
    -        recovery_failed = False
    -        pointer_restore_failed = False
    -
    -        def fail_owner_commit(*_args: object) -> None:
    -            raise RuntimeError("candidate owner commit failed")
    -
    -        async def fail_recovery_once(*args: object, **kwargs: object) -> None:
    -            nonlocal recovery_failed
    -            if not recovery_failed:
    -                recovery_failed = True
    -                raise RuntimeError("stable Root rebuild failed")
    -            await original_recover(*args, **kwargs)  # type: ignore[arg-type]
    -
    -        def fail_pointer_restore_once(*args: object, **kwargs: object):
    -            nonlocal pointer_restore_failed
    -            if (
    -                not pointer_restore_failed
    -                and kwargs.get("stable") == stable_pointer
    -                and kwargs.get("latest") == latest_pointer
    -            ):
    -                pointer_restore_failed = True
    -                raise RuntimeError("stable pointer restore failed")
    -            return original_write_pointers(*args, **kwargs)  # type: ignore[arg-type]
    -
    -        monkeypatch.setattr(
    -            manager,
    -            "_activate_published_generation",
    -            fail_owner_commit,
    -        )
    -        monkeypatch.setattr(manager, "_recover_stable_root", fail_recovery_once)
    -        monkeypatch.setattr(
    -            plugin_manager_module,
    -            "write_pointers",
    -            fail_pointer_restore_once,
    -        )
    -        with pytest.raises(RuntimeError, match="formal recovery"):
    -            await manager.switch_ready("installed_v3@lab")
    -        record = manager.reload_journal.get(candidate.reload_tx_id)
    -        assert record.phase == "degraded"
    -        assert record.recovery_target == "base"
    -        assert read_pointer(plugin_base, "stable") == latest_pointer
    -        assert read_pointer(plugin_base, "latest") == latest_pointer
    -        monkeypatch.setattr(manager, "_recover_stable_root", original_recover)
    -        recovered = await manager.retry_runtime_recovery("installed_v3@lab")
    -        assert recovered["publication_state"] == "recovered"
    -    else:
    -        result = await manager.switch_ready("installed_v3@lab")
    -        assert result["publication_state"] == "promoted"
    -        promoted_snapshot = manager.current_snapshot
    -        assert promoted_snapshot is not None
    -        promoted_root = promoted_snapshot.composition_root
    -        assert promoted_root is not None and promoted_root is not old_root
    -        assert promoted_root.instance_token in manager._runtime_started_roots
    -        assert old_root.instance_token not in manager._runtime_started_roots
    -        assert manager.generation("installed_v3@lab") is candidate
    -        assert manager.ready_candidate is None
    -        assert candidate.instance.module.started_roots == [
    -            id(promoted_root.instance_token)
    -        ]
    -        assert candidate.instance.module.stopped_roots == []
    -        assert stable.instance.module.stopped_roots == [id(old_root.instance_token)]
    -        assert read_pointer(plugin_base, "stable") == latest_pointer
    -        assert read_pointer(plugin_base, "latest") == latest_pointer
    -        runtime_services.cancel()
    -        with pytest.raises(asyncio.CancelledError):
    -            await runtime_services
    -        await manager.terminate_all()
    -        return
    -
    -    assert manager.current_snapshot is stable_snapshot
    -    assert manager.generation("installed_v3@lab") is stable
    -    assert manager.ready_candidate is None
    -    assert manager.latest_snapshot is stable_snapshot
    -    replacement_root = stable_snapshot.composition_root
    -    assert replacement_root is not None and replacement_root is not old_root
    -    assert replacement_root.instance_token in manager._runtime_started_roots
    -    assert old_root.instance_token not in manager._runtime_started_roots
    -    assert candidate.instance.module.disposed is True
    -    assert candidate.scope.closed is True
    -    assert read_pointer(plugin_base, "stable") == stable_pointer
    -    assert read_pointer(plugin_base, "latest") == latest_pointer
    -    assert stable.instance.module.disposed is False
    -    assert stable.instance.module.started_roots == [
    -        id(old_root.instance_token),
    -        id(replacement_root.instance_token),
    -    ]
    -    assert stable.instance.module.stopped_roots == [id(old_root.instance_token)]
    -    # A candidate that never became public must not receive runtime lifecycle.
    -    assert candidate.instance.module.started_roots == []
    -    assert candidate.instance.module.stopped_roots == []
    -    monkeypatch.setattr(manager, "_activate_published_generation", original_activate)
    -    runtime_services.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await runtime_services
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_mcp_slots.py b/tests/test_plugin_composition_mcp_slots.py
    deleted file mode 100644
    index e31710623..000000000
    --- a/tests/test_plugin_composition_mcp_slots.py
    +++ /dev/null
    @@ -1,2385 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import os
    -import sys
    -import socket
    -import shutil
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    MCP_SERVERS,
    -    CompositionError,
    -    CompositionRoot,
    -    EndpointEnv,
    -    McpServerDefinition,
    -    PluginRuntime,
    -)
    -from agent.plugin_composition.channels import (
    -    ChannelCapability,
    -    ChannelDeliveryReceipt,
    -    ChannelFactoryContext,
    -    ChannelReady,
    -    CoreChannelDefinition,
    -    DeliveryStatus,
    -    ProviderDeliveryReceipt,
    -    ProviderDeliveryRequest,
    -    StopReceipt,
    -)
    -from agent.control.models import TurnRequest
    -from agent.control.ports import ControlExecutionResult
    -from agent.control.runtime import ConversationRuntime
    -from agent.plugin_composition.mcp_slots import (
    -    PluginMcpServers,
    -    _freeze_plugin_mcp_servers,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.artifacts import ArtifactPointer, read_pointers, write_pointers
    -from agent.plugins.manifest import write_plugin_manifest
    -from agent.plugins.snapshot import (
    -    RuntimeSnapshot,
    -    bind_runtime_snapshot,
    -    reset_runtime_snapshot,
    -)
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from bus.events import InboundMessage
    -from session.store import SessionStore
    -from utils.process_group import OwnedProcessGroup
    -
    -
    -def _runtime(plugin_dir: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id=plugin_dir.name,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _definition(*, candidate_backend: str = "recording") -> McpServerDefinition:
    -    return McpServerDefinition(
    -        name="calendar",
    -        command=("python", "mcp.py"),
    -        cwd=".",
    -        env={"MODE": "stdio"},
    -        required_tools=("get_events",),
    -        candidate_read_only_tools=("get_events",),
    -        endpoint_env=(EndpointEnv("PORT", "calendar_api"),),
    -        candidate_env={"CALENDAR_BACKEND": candidate_backend},
    -    )
    -
    -
    -def _plugin_dir(root: Path, name: str = "calendar") -> Path:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "mcp.py").write_text("print('probe')\n", encoding="utf-8")
    -    return plugin_dir
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_registry_freezes_descriptor_health_and_cleanup(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _plugin_dir(tmp_path)
    -    root = CompositionRoot("mcp-registry")
    -    servers = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MCP_SERVERS, servers)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(MCP_SERVERS).register(ctx, _definition())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MCP_SERVERS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    registry = _freeze_plugin_mcp_servers(servers, root.instance_token)
    -    binding = registry["calendar"]
    -    assert binding.descriptor.owner == "calendar"
    -    assert binding.descriptor.endpoint_env == (
    -        EndpointEnv("PORT", "calendar_api"),
    -    )
    -    assert binding.health.healthy
    -    assert binding.is_live()
    -    incident = binding.incident_reporter(
    -        "mcp_handshake_failed",
    -        "calendar initialize failed",
    -    )
    -    assert incident.owner == "calendar"
    -    assert root.recent_incidents() == (incident,)
    -
    -    await fiber.dispose()
    -    assert _freeze_plugin_mcp_servers(servers, root.instance_token) is registry
    -    assert not binding.is_live()
    -    assert root.receipt().effects == ("root:service:core.mcp_servers",)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_registry_rejects_duplicate_frozen_and_reserved_env(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _plugin_dir(tmp_path)
    -    root = CompositionRoot("mcp-invalid")
    -    servers = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MCP_SERVERS, servers)
    -    captured = None
    -
    -    async def apply(ctx) -> None:
    -        nonlocal captured
    -        captured = ctx
    -        await ctx.require(MCP_SERVERS).register(ctx, _definition())
    -
    -    _ = await root.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MCP_SERVERS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    _ = _freeze_plugin_mcp_servers(servers, root.instance_token)
    -    assert captured is not None
    -    with pytest.raises(CompositionError, match="已冻结"):
    -        await servers.register(captured, _definition())
    -    await root.dispose()
    -
    -    root = CompositionRoot("mcp-reserved")
    -    servers = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MCP_SERVERS, servers)
    -
    -    async def reserved(ctx) -> None:
    -        definition = McpServerDefinition(
    -            name="calendar",
    -            command=("python",),
    -            env={"AKASHIC_WORKSPACE": "/tmp/escape"},
    -        )
    -        await ctx.require(MCP_SERVERS).register(ctx, definition)
    -
    -    _ = await root.mount(
    -        reserved,
    -        name="calendar",
    -        inject=(MCP_SERVERS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    assert not root.receipt().ready
    -    assert any(
    -        "env 无效" in (fiber.error or "") for fiber in root.receipt().fibers
    -    )
    -    assert len(_freeze_plugin_mcp_servers(servers, root.instance_token)) == 0
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_registry_identity_ignores_runtime_root(tmp_path: Path) -> None:
    -    identities: list[str] = []
    -    for suffix in ("candidate", "formal"):
    -        plugin_dir = _plugin_dir(tmp_path / suffix)
    -        root = CompositionRoot(f"mcp-{suffix}")
    -        servers = PluginMcpServers(root.instance_token)
    -        _ = await root.context.provide(MCP_SERVERS, servers)
    -
    -        async def apply(ctx) -> None:
    -            await ctx.require(MCP_SERVERS).register(ctx, _definition())
    -
    -        _ = await root.mount(
    -            apply,
    -            name="calendar",
    -            inject=(MCP_SERVERS,),
    -            runtime=_runtime(plugin_dir),
    -        )
    -        identities.append(
    -            _freeze_plugin_mcp_servers(servers, root.instance_token).identity
    -        )
    -        await root.dispose()
    -    assert identities[0] == identities[1]
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_registry_rejects_missing_command_and_escaped_cwd(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _plugin_dir(tmp_path)
    -    outside = tmp_path / "outside"
    -    outside.mkdir()
    -    (plugin_dir / "outside-link").symlink_to(outside, target_is_directory=True)
    -    definitions = (
    -        McpServerDefinition(name="missing", command=("python", "missing.py")),
    -        McpServerDefinition(name="escaped", command=("python",), cwd="outside-link"),
    -    )
    -    for index, definition in enumerate(definitions):
    -        root = CompositionRoot(f"mcp-path-{index}")
    -        servers = PluginMcpServers(root.instance_token)
    -        _ = await root.context.provide(MCP_SERVERS, servers)
    -
    -        async def apply(ctx, definition=definition) -> None:
    -            await ctx.require(MCP_SERVERS).register(ctx, definition)
    -
    -        _ = await root.mount(
    -            apply,
    -            name="calendar",
    -            inject=(MCP_SERVERS,),
    -            runtime=_runtime(plugin_dir),
    -        )
    -        assert not root.receipt().ready
    -        assert (
    -            len(_freeze_plugin_mcp_servers(servers, root.instance_token)) == 0
    -        )
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugins_cannot_freeze_shared_mcp_declarations_early(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("mcp-core-freeze-owner")
    -    servers = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MCP_SERVERS, servers)
    -
    -    definitions = (
    -        _definition(),
    -        McpServerDefinition(name="contacts", command=("python", "mcp.py")),
    -    )
    -    for name, definition in zip(("calendar", "contacts"), definitions, strict=True):
    -        plugin_dir = _plugin_dir(tmp_path, name)
    -
    -        async def apply(ctx, definition=definition, name=name) -> None:
    -            service = ctx.require(MCP_SERVERS)
    -            if name == "calendar":
    -                with pytest.raises(AttributeError):
    -                    _ = getattr(service, "freeze")
    -            await service.register(ctx, definition)
    -
    -        fiber = await root.mount(
    -            apply,
    -            name=name,
    -            inject=(MCP_SERVERS,),
    -            runtime=_runtime(plugin_dir),
    -        )
    -        assert fiber.state.value == "active"
    -
    -    frozen = _freeze_plugin_mcp_servers(servers, root.instance_token)
    -    assert tuple(frozen) == ("calendar", "contacts")
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_mcp_registry_rejects_context_from_another_root(
    -    tmp_path: Path,
    -) -> None:
    -    root_a = CompositionRoot("mcp-root-a")
    -    root_b = CompositionRoot("mcp-root-b")
    -    servers_a = PluginMcpServers(root_a.instance_token)
    -    servers_b = PluginMcpServers(root_b.instance_token)
    -    _ = await root_a.context.provide(MCP_SERVERS, servers_a)
    -    _ = await root_b.context.provide(MCP_SERVERS, servers_b)
    -    plugin_dir = _plugin_dir(tmp_path)
    -
    -    async def apply(ctx) -> None:
    -        await servers_a.register(ctx, _definition())
    -
    -    _ = await root_b.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MCP_SERVERS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -
    -    assert any(
    -        "插件 MCP 声明 Service 不属于当前 Root" in (fiber.error or "")
    -        for fiber in root_b.receipt().fibers
    -    )
    -    assert root_a.receipt().health == ()
    -    assert root_b.receipt().health == ()
    -    assert root_a.receipt().effects == ("root:service:core.mcp_servers",)
    -    assert root_b.receipt().effects == ("root:service:core.mcp_servers",)
    -    assert len(_freeze_plugin_mcp_servers(servers_a, root_a.instance_token)) == 0
    -    assert len(_freeze_plugin_mcp_servers(servers_b, root_b.instance_token)) == 0
    -
    -    await root_b.dispose()
    -    await root_a.dispose()
    -
    -
    -def _manager(
    -    tmp_path: Path,
    -    *,
    -    tool_registry: ToolRegistry | None = None,
    -) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=tool_registry,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -class _CoreChannelAdapter:
    -    def __init__(self, binding_token: str) -> None:
    -        self._binding_token = binding_token
    -
    -    async def start(self) -> ChannelReady:
    -        return ChannelReady(self._binding_token)
    -
    -    async def deliver(
    -        self,
    -        request: ProviderDeliveryRequest,
    -    ) -> ProviderDeliveryReceipt:
    -        return ProviderDeliveryReceipt(request.delivery_id, DeliveryStatus.DELIVERED)
    -
    -    async def stop(self) -> StopReceipt:
    -        return StopReceipt(self._binding_token, resources_closed=True)
    -
    -
    -def _core_channel_definition() -> CoreChannelDefinition:
    -    def factory(context: ChannelFactoryContext) -> _CoreChannelAdapter:
    -        return _CoreChannelAdapter(context.binding_token)
    -
    -    return CoreChannelDefinition(
    -        name="web",
    -        capabilities=frozenset({ChannelCapability.OUTBOUND}),
    -        factory=factory,
    -        inbound_identity=None,
    -        source_revision="core-native-v3",
    -        config_revision="core-native-v3",
    -        generation_id="core-native-v3",
    -    )
    -
    -
    -def _port_live(port: int) -> bool:
    -    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
    -        return probe.connect_ex(("127.0.0.1", port)) == 0
    -
    -
    -def _plugin_source(version: str, *, python_command: str = "python") -> str:
    -    return (
    -        "from agent.plugin_composition import (\n"
    -        "    MANAGED_PROCESSES, MCP_SERVERS, EndpointEnv,\n"
    -        "    ManagedProcessDefinition, McpServerDefinition,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'calendar'\n"
    -        f"version = '{version}'\n"
    -        "inject = (MCP_SERVERS, MANAGED_PROCESSES)\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(MANAGED_PROCESSES).register(\n"
    -        "        ctx, ManagedProcessDefinition(\n"
    -        f"            name='calendar_api', command=({python_command!r}, 'api.py'),\n"
    -        "            formal_port=18000, readiness_path='/health',\n"
    -        "        ),\n"
    -        "    )\n"
    -        "    await ctx.require(MCP_SERVERS).register(\n"
    -        "        ctx, McpServerDefinition(\n"
    -        f"            name='calendar', command=({python_command!r}, 'mcp.py'),\n"
    -        "            required_tools=('get_events',),\n"
    -        "            candidate_read_only_tools=('get_events',),\n"
    -        "            endpoint_env=(EndpointEnv('PORT', 'calendar_api'),),\n"
    -        f"            candidate_env={{'VERSION': '{version}'}},\n"
    -        "        ),\n"
    -        "    )\n"
    -    )
    -
    -
    -def _write_static_manager_plugin(tmp_path: Path, version: str) -> Path:
    -    plugin_dir = _plugin_dir(tmp_path / "plugins")
    -    (plugin_dir / "entry.py").write_text(
    -        _plugin_source(version),
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "api.py").write_text(
    -        "import os\n"
    -        "from http.server import BaseHTTPRequestHandler, HTTPServer\n"
    -        "class Handler(BaseHTTPRequestHandler):\n"
    -        "    def do_GET(self):\n"
    -        "        self.send_response(200); self.end_headers(); self.wfile.write(b'ready')\n"
    -        "    def log_message(self, *_args): pass\n"
    -        "HTTPServer(('127.0.0.1', int(os.environ['PORT'])), Handler).serve_forever()\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "mcp.py").write_text(
    -        "import json, os, sys\n"
    -        "for raw in sys.stdin:\n"
    -        "    msg = json.loads(raw); method = msg.get('method')\n"
    -        "    if method == 'initialize': result = {'protocolVersion': '2025-11-25'}\n"
    -        "    elif method == 'tools/list': result = {'tools': [{'name': 'get_events', "
    -        "'description': 'read events', 'inputSchema': {'type': 'object'}}]}\n"
    -        "    elif method == 'tools/call': result = {'content': [{'type': 'text', "
    -        "'text': '|'.join((os.environ.get('VERSION', 'formal'), "
    -        "os.environ['PORT'], os.environ['AKA_PLUGIN_DATA_DIR']))}]}\n"
    -        "    else: continue\n"
    -        "    print(json.dumps({'jsonrpc': '2.0', 'id': msg['id'], 'result': result}), flush=True)\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "requirements.txt").write_text("", encoding="utf-8")
    -    interpreter = plugin_dir / ".venv" / "bin" / "python"
    -    interpreter.parent.mkdir(parents=True)
    -    interpreter.write_text(
    -        f"#!/bin/sh\nexec {sys.executable} \"$@\"\n",
    -        encoding="utf-8",
    -    )
    -    interpreter.chmod(0o755)
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = \"calendar\"\n"
    -        f"version = \"{version}\"\n"
    -        "api_version = 3\n"
    -        "entrypoint = \"entry.py\"\n\n"
    -        "[[python]]\n"
    -        "requirements = \"requirements.txt\"\n\n"
    -        "[[mcp]]\n"
    -        "name = \"calendar\"\n"
    -        "command = [\"python\", \"mcp.py\"]\n"
    -        "required_tools = [\"get_events\"]\n"
    -        "candidate_read_only_tools = [\"get_events\"]\n"
    -        "endpoint_env = [{env = \"PORT\", process = \"calendar_api\"}]\n"
    -        f"candidate_env = {{VERSION = \"{version}\"}}\n\n"
    -        "[[processes]]\n"
    -        "name = \"calendar_api\"\n"
    -        "command = [\"python\", \"api.py\"]\n"
    -        "port_env = \"PORT\"\n"
    -        "formal_port = 18000\n"
    -        "readiness_path = \"/health\"\n",
    -        encoding="utf-8",
    -    )
    -    return plugin_dir
    -
    -
    -def _upgrade_static_manager_plugin(plugin_dir: Path, version: str) -> None:
    -    (plugin_dir / "entry.py").write_text(
    -        _plugin_source(version),
    -        encoding="utf-8",
    -    )
    -    manifest = plugin_dir / "akashic.plugin.toml"
    -    text = manifest.read_text(encoding="utf-8")
    -    manifest.write_text(
    -        text.replace('version = "1"', f'version = "{version}"').replace(
    -            'VERSION = "1"',
    -            f'VERSION = "{version}"',
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_static_manifest_is_admission_source_and_reconciles_mcp_root(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -
    -    await manager.load_all()
    -    generation = manager._active_generations["calendar"]  # pyright: ignore[reportPrivateUsage]
    -    assert generation.entrypoint == "entry.py"
    -    assert generation.static_manifest is not None
    -    assert generation.static_manifest.mcp_servers[0].name == "calendar"
    -    assert generation.static_manifest.managed_processes[0].name == "calendar_api"
    -    runtime_commands = dict(generation.static_runtime_commands)
    -    assert runtime_commands["mcp:calendar"][0] == str(
    -        plugin_dir / ".venv" / "bin" / "python"
    -    )
    -    assert runtime_commands["process:calendar_api"][0] == str(
    -        plugin_dir / ".venv" / "bin" / "python"
    -    )
    -    assert manager.current_snapshot is not None
    -    assert manager.current_snapshot.mcp_server_registry is not None
    -    assert manager.current_snapshot.managed_process_registry is not None
    -    runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -        generation.generation_id
    -    )
    -    assert runtime is not None and runtime.processes is not None
    -    endpoint = runtime.processes.endpoint("calendar_api")
    -    assert endpoint.port == 18000 and _port_live(endpoint.port)
    -    tool_registry = manager.current_snapshot.tool_registry
    -    assert tool_registry is not None
    -    tool = tool_registry.get_tool("mcp_calendar__get_events")
    -    assert tool is not None
    -    assert await tool.execute() == "|".join(
    -        ("formal", "18000", str(generation.data_dir))
    -    )
    -
    -    (plugin_dir / "entry.py").write_text(
    -        _plugin_source("2"),
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        (plugin_dir / "akashic.plugin.toml")
    -        .read_text(encoding="utf-8")
    -        .replace('version = "1"', 'version = "2"')
    -        .replace('VERSION = "1"', 'VERSION = "2"'),
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None
    -    assert candidate.entrypoint == "entry.py"
    -    assert candidate.static_manifest is not None
    -    await manager.discard_prepared("calendar")
    -    await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_unrelated_mcp_reload_keeps_builtin_runtimes_root_local(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    """Keep unchanged builtin runtimes isolated while an MCP plugin reloads."""
    -
    -    # 1. Start Scheduler, Subagent, and a real MCP plugin in one stable Root.
    -    calendar_dir = _write_static_manager_plugin(tmp_path, "1")
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    store = SessionStore(workspace / "sessions.db")
    -    executions: list[TurnRequest] = []
    -
    -    async def execute(request: TurnRequest) -> ControlExecutionResult:
    -        executions.append(request)
    -        return ControlExecutionResult(response=f"child:{request.input}")
    -
    -    async def publish(_message: InboundMessage) -> None:
    -        raise AssertionError("synchronous fixture must not publish a continuation")
    -
    -    async def deliver(_message: object) -> ChannelDeliveryReceipt:
    -        raise AssertionError("empty scheduler must not deliver")
    -
    -    conversation = ConversationRuntime(store, execute)
    -    builtin_root = Path(__file__).resolve().parents[1] / "plugins"
    -    manager = PluginManager(
    -        plugin_dirs=[
    -            tmp_path / "plugins",
    -            builtin_root / "scheduler",
    -            builtin_root / "subagent",
    -        ],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        conversation,
    -        programmatic_session_creator=store.create_session,
    -        programmatic_session_reader=store.get_session_meta,
    -    )
    -    manager.bind_continuation_publisher(publish)
    -    manager.bind_delivery_sender(deliver)
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -    old_snapshot = manager.current_snapshot
    -    assert old_snapshot is not None and old_snapshot.composition_root is not None
    -    old_lease = manager.snapshot_store.lease()
    -    stop_entered = asyncio.Event()
    -    release_stop = asyncio.Event()
    -
    -    async def execute_tool(snapshot, lease, name, arguments, turn_id):
    -        assert snapshot.tool_registry is not None
    -        token = bind_runtime_snapshot(lease)
    -        snapshot.tool_registry.set_context(turn_id=turn_id)
    -        try:
    -            return await snapshot.tool_registry.execute(
    -                name,
    -                arguments,
    -                raise_errors=True,
    -            )
    -        finally:
    -            reset_runtime_snapshot(token)
    -
    -    try:
    -        for _ in range(200):
    -            if (
    -                old_snapshot.composition_root.instance_token
    -                in manager._runtime_started_roots  # pyright: ignore[reportPrivateUsage]
    -            ):
    -                break
    -            await asyncio.sleep(0.01)
    -        else:
    -            raise AssertionError("old Root runtime did not start")
    -
    -        old_spawn = await execute_tool(
    -            old_snapshot,
    -            old_lease,
    -            "spawn",
    -            {"task": "old-root"},
    -            "parent:old",
    -        )
    -        assert "child:old-root" in old_spawn
    -
    -        # 2. Prepare MCP v2 while a Turn holds the old Root, then publish it.
    -        _upgrade_static_manager_plugin(calendar_dir, "2")
    -        candidate = await manager.prepare_candidate("calendar")
    -        assert candidate is not None and candidate.runtime_snapshot is not None
    -        assert old_snapshot.lease_count == 1
    -        await old_lease.release()
    -
    -        original_stop = manager._stop_runtime_snapshot  # pyright: ignore[reportPrivateUsage]
    -
    -        async def gated_stop(snapshot) -> None:
    -            if snapshot is old_snapshot:
    -                stop_entered.set()
    -                await release_stop.wait()
    -            await original_stop(snapshot)
    -
    -        monkeypatch.setattr(manager, "_stop_runtime_snapshot", gated_stop)
    -        publication = asyncio.create_task(manager.publish_prepared("calendar"))
    -        await asyncio.wait_for(stop_entered.wait(), timeout=5)
    -        assert not publication.done()
    -        assert manager.current_snapshot is old_snapshot
    -        release_stop.set()
    -        result = await publication
    -        assert result["publication_state"] == "committed"
    -        new_snapshot = manager.current_snapshot
    -        assert new_snapshot is not None and new_snapshot is not old_snapshot
    -        assert old_snapshot.lease_count == 0
    -        assert old_snapshot.composition_root is not None
    -        assert new_snapshot.composition_root is not None
    -
    -        for plugin_id in ("scheduler", "subagent"):
    -            assert (
    -                old_snapshot.generations[plugin_id].instance.module
    -                is new_snapshot.generations[plugin_id].instance.module
    -            )
    -        assert old_snapshot.plugin_tool_catalog is not None
    -        assert new_snapshot.plugin_tool_catalog is not None
    -        for tool_name in ("list_schedules", "spawn", "spawn_manage"):
    -            old_binding = old_snapshot.plugin_tool_catalog[tool_name]
    -            new_binding = new_snapshot.plugin_tool_catalog[tool_name]
    -            assert old_binding.handler is not None
    -            assert new_binding.handler is not None
    -            assert old_binding.handler is not new_binding.handler
    -            assert not old_binding.is_live()
    -            assert new_binding.is_live()
    -
    -        # 3. The old Root is closed before commit; only the new Root admits work.
    -        for _ in range(500):
    -            if not old_snapshot.plugin_tool_catalog["spawn"].is_live():
    -                break
    -            await asyncio.sleep(0.01)
    -        else:
    -            raise AssertionError("old Root did not drain")
    -
    -        new_lease = manager.snapshot_store.lease()
    -        try:
    -            new_spawn = await execute_tool(
    -                new_snapshot,
    -                new_lease,
    -                "spawn",
    -                {"task": "new-root"},
    -                "parent:new",
    -            )
    -            new_schedules = await execute_tool(
    -                new_snapshot,
    -                new_lease,
    -                "list_schedules",
    -                {},
    -                "parent:new-list",
    -            )
    -        finally:
    -            await new_lease.release()
    -        assert "child:new-root" in new_spawn
    -        assert new_schedules == "当前没有待执行的定时任务"
    -        assert [request.input for request in executions] == ["old-root", "new-root"]
    -    finally:
    -        release_stop.set()
    -        if old_snapshot.lease_count:
    -            await old_lease.release()
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -        await conversation.shutdown()
    -        store.close()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_core_channel_rebind_preserves_live_mcp_tools(tmp_path: Path) -> None:
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -
    -    try:
    -        await manager.load_all()
    -        await manager.bind_core_channel_definitions((_core_channel_definition(),))
    -
    -        snapshot = manager.current_snapshot
    -        assert snapshot is not None and snapshot.tool_registry is not None
    -        tool = snapshot.tool_registry.get_tool("mcp_calendar__get_events")
    -        assert tool is not None
    -        assert await tool.execute() == "|".join(
    -            (
    -                "formal",
    -                "18000",
    -                str(tmp_path / "workspace/plugin-data/calendar-builtin"),
    -            )
    -        )
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_first_plugin_failure_rebuilds_core_only_formal_root(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    await manager.bind_core_channel_definitions((_core_channel_definition(),))
    -    stable_snapshot = manager.current_snapshot
    -    assert stable_snapshot is not None and stable_snapshot.generations == {}
    -    old_root = stable_snapshot.composition_root
    -    assert old_root is not None
    -
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None
    -    original_start = manager._composition_generation_host.start  # pyright: ignore[reportPrivateUsage]
    -    formal_failed = False
    -
    -    async def fail_formal_once(*args, **kwargs):
    -        nonlocal formal_failed
    -        if kwargs.get("mode") == "formal" and not formal_failed:
    -            formal_failed = True
    -            raise RuntimeError("first plugin formal start failed")
    -        return await original_start(*args, **kwargs)
    -
    -    monkeypatch.setattr(
    -        manager._composition_generation_host,  # pyright: ignore[reportPrivateUsage]
    -        "start",
    -        fail_formal_once,
    -    )
    -    with pytest.raises(RuntimeError, match="first plugin formal start failed"):
    -        await manager.publish_prepared("calendar")
    -
    -    replacement = manager.current_snapshot
    -    assert replacement is stable_snapshot
    -    assert replacement.generations == {}
    -    assert replacement.composition_root is not None
    -    assert replacement.composition_root is not old_root
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_builtin_direct_formal_failure_recovers_stable_runtime_explicitly(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-builtin-recovery")
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.generation("calendar")
    -        stable_snapshot = manager.current_snapshot
    -        assert stable is not None and stable_snapshot is not None
    -        assert _port_live(18000)
    -        _upgrade_static_manager_plugin(plugin_dir, "2")
    -        candidate = await manager.prepare_candidate("calendar")
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -        original_start = manager._composition_generation_host.start  # pyright: ignore[reportPrivateUsage]
    -        original_restore = manager._recover_stable_root  # pyright: ignore[reportPrivateUsage]
    -        formal_failed = False
    -        restore_failures = 0
    -
    -        async def fail_candidate_formal_once(*args, **kwargs):
    -            nonlocal formal_failed
    -            if kwargs.get("mode") == "formal" and not formal_failed:
    -                assert manager.current_snapshot is stable_snapshot
    -                assert not stable_snapshot.accepting_leases
    -                provisional = manager.latest_snapshot
    -                assert provisional is not None and provisional is not stable_snapshot
    -                assert not provisional.accepting_leases
    -                formal_failed = True
    -                raise RuntimeError("builtin candidate formal start failed")
    -            return await original_start(*args, **kwargs)
    -
    -        async def fail_stable_restore_once(*args, **kwargs):
    -            nonlocal restore_failures
    -            if restore_failures < 1:
    -                restore_failures += 1
    -                raise RuntimeError("builtin stable restore failed")
    -            return await original_restore(*args, **kwargs)
    -
    -        monkeypatch.setattr(
    -            manager._composition_generation_host,  # pyright: ignore[reportPrivateUsage]
    -            "start",
    -            fail_candidate_formal_once,
    -        )
    -        monkeypatch.setattr(
    -            manager,
    -            "_recover_stable_root",
    -            fail_stable_restore_once,
    -        )
    -
    -        with pytest.raises(RuntimeError, match="builtin candidate formal start failed"):
    -            await manager.publish_prepared("calendar")
    -
    -        record = manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        )
    -        assert record.phase == "degraded"
    -        assert record.recovery_target == "base"
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.generation("calendar") is stable
    -        assert not _port_live(18000)
    -
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -
    -        assert recovered["publication_state"] == "recovered"
    -        assert recovered["recovery_target"] == "base"
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.generation("calendar") is stable
    -        assert _port_live(18000)
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "recovered"
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_handoff_retries_old_root_stop_before_rebuild(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    entry = plugin_dir / "entry.py"
    -    source = entry.read_text(encoding="utf-8")
    -    source = source.replace(
    -        "async def apply(ctx, config):\n",
    -        "stop_attempts = 0\n"
    -        "async def stop_once(_event):\n"
    -        "    global stop_attempts\n"
    -        "    stop_attempts += 1\n"
    -        "    if stop_attempts == 1:\n"
    -        "        raise RuntimeError('stable stop failed once')\n"
    -        "async def apply(ctx, config):\n",
    -    )
    -    entry.write_text(
    -        source
    -        + "    from agent.plugin_composition import RUNTIME_STOPPING\n"
    -        + "    await ctx.on(RUNTIME_STOPPING, stop_once)\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.generation("calendar")
    -        stable_snapshot = manager.current_snapshot
    -        assert stable is not None and stable_snapshot is not None
    -        await manager._start_runtime_snapshot(stable_snapshot)  # pyright: ignore[reportPrivateUsage]
    -        assert _port_live(18000)
    -
    -        _upgrade_static_manager_plugin(plugin_dir, "2")
    -        candidate = await manager.prepare_candidate("calendar")
    -        assert candidate is not None
    -        with pytest.raises(RuntimeError, match="stable stop failed once"):
    -            await manager.publish_prepared("calendar")
    -
    -        assert manager.current_snapshot is stable_snapshot
    -        assert stable.instance.module.stop_attempts == 2
    -        assert _port_live(18000)
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_runtime_candidate_isolated_and_commit_failure_restores_stable(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-runtime-v1")
    -    monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, latest_artifact)
    -    latest_entry = latest_artifact / "entry.py"
    -    latest_entry.write_text(_plugin_source("2"), encoding="utf-8")
    -    latest_manifest = latest_artifact / "akashic.plugin.toml"
    -    latest_manifest.write_text(
    -        latest_manifest.read_text(encoding="utf-8")
    -        .replace('version = "1"', 'version = "2"')
    -        .replace('VERSION = "1"', 'VERSION = "2"'),
    -        encoding="utf-8",
    -    )
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.generation("calendar@lab")
    -        stable_snapshot = manager.current_snapshot
    -        assert stable is not None and stable_snapshot is not None
    -        assert _port_live(18000)
    -
    -        write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -        result = (await manager.reconcile_changed())[0]
    -        candidate = manager.ready_candidate
    -        assert result.get("publication_state") == "latest_ready", result
    -        assert candidate is not None and candidate.runtime_snapshot is not None
    -        assert candidate.reload_tx_id is not None
    -        journal_record = manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        )
    -        assert journal_record.runtime_owner_boot_id == "boot-runtime-v1"
    -        assert journal_record.base_artifact_pointer == stable_pointer.path
    -        assert journal_record.candidate_artifact_pointer == latest_pointer.path
    -        candidate_runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        )
    -        assert candidate_runtime is not None and candidate_runtime.processes is not None
    -        candidate_port = candidate_runtime.processes.endpoint("calendar_api").port
    -        assert candidate_port != 18000 and _port_live(candidate_port)
    -        candidate_tool = candidate.runtime_snapshot.tool_registry.get_tool(  # type: ignore[union-attr]
    -            "mcp_calendar__get_events"
    -        )
    -        assert candidate_tool is not None
    -        candidate_output = await candidate_tool.execute()
    -        assert candidate_output.startswith(f"2|{candidate_port}|")
    -        assert isinstance(candidate_output, str)
    -        assert "plugin-validation" in candidate_output
    -        assert manager.current_snapshot is stable_snapshot
    -        assert _port_live(18000)
    -
    -        original_host_start = manager._composition_generation_host.start  # pyright: ignore[reportPrivateUsage]
    -        original_restore = manager._recover_stable_root  # pyright: ignore[reportPrivateUsage]
    -        formal_failed = False
    -        restore_failures = 0
    -
    -        async def fail_candidate_formal_once(*args, **kwargs):
    -            nonlocal formal_failed
    -            if kwargs.get("mode") == "formal" and not formal_failed:
    -                assert manager.current_snapshot is stable_snapshot
    -                assert not stable_snapshot.accepting_leases
    -                provisional = manager.latest_snapshot
    -                assert provisional is not None and provisional is not stable_snapshot
    -                assert not provisional.accepting_leases
    -                formal_failed = True
    -                raise RuntimeError("candidate formal start failed")
    -            return await original_host_start(*args, **kwargs)
    -
    -        async def fail_stable_restore_once(*args, **kwargs):
    -            nonlocal restore_failures
    -            if restore_failures < 1:
    -                restore_failures += 1
    -                raise RuntimeError("stable runtime restore failed")
    -            return await original_restore(*args, **kwargs)
    -
    -        monkeypatch.setattr(
    -            manager._composition_generation_host,  # pyright: ignore[reportPrivateUsage]
    -            "start",
    -            fail_candidate_formal_once,
    -        )
    -        monkeypatch.setattr(
    -            manager,
    -            "_recover_stable_root",
    -            fail_stable_restore_once,
    -        )
    -        with pytest.raises(RuntimeError, match="candidate formalization 失败"):
    -            await manager.switch_ready("calendar@lab")
    -
    -        degraded = manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        )
    -        assert degraded.phase == "degraded"
    -        assert degraded.recovery_target == "base"
    -        assert manager.ready_candidate is not None
    -        assert not _port_live(18000) and not _port_live(candidate_port)
    -
    -        recovered = await manager.retry_runtime_recovery("calendar@lab")
    -
    -        assert recovered["publication_state"] == "recovered"
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.ready_candidate is None
    -        assert _port_live(18000)
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "recovered"
    -
    -        monkeypatch.undo()
    -        write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -        result = (await manager.reconcile_changed())[0]
    -        candidate = manager.ready_candidate
    -        assert result.get("publication_state") == "latest_ready", result
    -        assert candidate is not None
    -        candidate_runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        )
    -        assert candidate_runtime is not None and candidate_runtime.processes is not None
    -        candidate_port = candidate_runtime.processes.endpoint("calendar_api").port
    -
    -        def fail_owner_commit(*_args: object) -> None:
    -            raise RuntimeError("runtime owner commit failed")
    -
    -        monkeypatch.setattr(
    -            manager,
    -            "_activate_published_generation",
    -            fail_owner_commit,
    -        )
    -        with pytest.raises(RuntimeError, match="runtime owner commit failed"):
    -            await manager.switch_ready("calendar@lab")
    -
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.generation("calendar@lab") is stable
    -        assert manager.ready_candidate is None
    -        assert _port_live(18000)
    -        assert not _port_live(candidate_port)
    -        stable_tool = stable_snapshot.tool_registry.get_tool(  # type: ignore[union-attr]
    -            "mcp_calendar__get_events"
    -        )
    -        assert stable_tool is not None
    -        assert await stable_tool.execute() == "|".join(
    -            ("formal", "18000", str(stable.data_dir))
    -        )
    -
    -        monkeypatch.undo()
    -        write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -        retry_result = (await manager.reconcile_changed())[0]
    -        retry_candidate = manager.ready_candidate
    -        assert retry_result.get("publication_state") == "latest_ready", retry_result
    -        assert retry_candidate is not None
    -        retry_runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            retry_candidate.generation_id
    -        )
    -        assert retry_runtime is not None and retry_runtime.processes is not None
    -        retry_port = retry_runtime.processes.endpoint("calendar_api").port
    -        assert retry_port != 18000 and _port_live(retry_port)
    -
    -        promoted = await manager.switch_ready("calendar@lab")
    -
    -        current = manager.current_snapshot
    -        active = manager.generation("calendar@lab")
    -        assert promoted["publication_state"] == "promoted"
    -        assert current is not None and current is not stable_snapshot
    -        assert active is retry_candidate
    -        assert _port_live(18000) and not _port_live(retry_port)
    -        promoted_tool = current.tool_registry.get_tool(  # type: ignore[union-attr]
    -            "mcp_calendar__get_events"
    -        )
    -        assert promoted_tool is not None
    -        assert await promoted_tool.execute() == "|".join(
    -            ("formal", "18000", str(active.data_dir))
    -        )
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_committed_watchdog_failure_is_journaled_and_restartable(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-watchdog-failure")
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("calendar")
    -        assert generation is not None
    -        process_host = manager._composition_generation_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[generation.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions:
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -        assert action.phase == "degraded"
    -        assert action.action == "retry_runtime_recovery"
    -        assert action.recovery_target == "candidate"
    -        assert action.runtime_owner_boot_id == "boot-watchdog-failure"
    -        assert manager._composition_generation_host.failure(  # pyright: ignore[reportPrivateUsage]
    -            generation.generation_id
    -        ) is not None
    -
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -
    -        assert recovered["recovery_target"] == "candidate"
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            action.tx_id
    -        ).phase == "recovered"
    -        runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            generation.generation_id
    -        )
    -        assert runtime is not None and runtime.processes is not None
    -        assert _port_live(runtime.processes.endpoint("calendar_api").port)
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_watchdog_failure_shutdown_drains_healthy_sibling_and_keeps_target(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-watchdog-shutdown")
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    terminated = False
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("calendar")
    -        assert generation is not None
    -        composition_host = manager._composition_generation_host  # pyright: ignore[reportPrivateUsage]
    -        process_host = composition_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[generation.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions:
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None and action.recovery_target == "candidate"
    -        caplog.clear()
    -
    -        await manager.terminate_all()
    -        terminated = True
    -
    -        record = manager._reload_journal.get(action.tx_id)  # pyright: ignore[reportPrivateUsage]
    -        assert record.phase == "degraded"
    -        assert record.recovery_target == "candidate"
    -        assert composition_host._mcp_host.get(generation.generation_id) is None  # pyright: ignore[reportPrivateUsage]
    -        assert composition_host._mcp_host.tombstone(generation.generation_id) is None  # pyright: ignore[reportPrivateUsage]
    -        assert composition_host._process_host.get(generation.generation_id) is not None  # pyright: ignore[reportPrivateUsage]
    -        assert composition_host._process_host.tombstone(generation.generation_id) is not None  # pyright: ignore[reportPrivateUsage]
    -
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -        assert "composition-runtime" in str(recovered["retry_receipt"])
    -        assert manager._reload_journal.get(action.tx_id).phase == "recovered"  # pyright: ignore[reportPrivateUsage]
    -        assert composition_host.failure(generation.generation_id) is None
    -        assert not any(
    -            "observer 已失效" in record.message
    -            or "health callback failed" in record.message
    -            for record in caplog.records
    -        )
    -    finally:
    -        if not terminated:
    -            await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_supervised_boot_recovers_builtin_runtime_into_current_release(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-builtin-runtime-old")
    -    monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    old_manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    new_manager: PluginManager | None = None
    -    old_manager_closed = False
    -    try:
    -        await old_manager.load_all()
    -        old_generation = old_manager.generation("calendar")
    -        assert old_generation is not None
    -        composition_host = old_manager._composition_generation_host  # pyright: ignore[reportPrivateUsage]
    -        process_host = composition_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[old_generation.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -
    -        action = None
    -        for _ in range(100):
    -            actions = old_manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions:
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -        assert action.recovery_target == "candidate"
    -
    -        await old_manager.terminate_all()
    -        old_manager_closed = True
    -        _upgrade_static_manager_plugin(plugin_dir, "2")
    -        monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-builtin-runtime-new")
    -        new_manager = _manager(
    -            tmp_path,
    -            tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -        )
    -        await new_manager.load_all()
    -
    -        stable = new_manager.generation("calendar")
    -        snapshot = new_manager.current_snapshot
    -        assert stable is not None and stable.source_type == "builtin"
    -        assert stable.static_manifest is not None
    -        assert stable.static_manifest.version == "2"
    -        assert stable.source_revision != old_generation.source_revision
    -        assert snapshot is not None and snapshot.tool_registry is not None
    -        tool = snapshot.tool_registry.get_tool("mcp_calendar__get_events")
    -        assert tool is not None
    -        assert (await tool.execute()).startswith("formal|18000|")
    -        assert new_manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            action.tx_id
    -        ).phase == "recovered"
    -    finally:
    -        if new_manager is not None:
    -            await new_manager.terminate_all()
    -        if not old_manager_closed:
    -            await old_manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_watchdog_failure_joins_prepared_candidate_transaction(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-watchdog-prepared")
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.generation("calendar")
    -        stable_snapshot = manager.current_snapshot
    -        assert stable is not None and stable_snapshot is not None
    -        _upgrade_static_manager_plugin(plugin_dir, "2")
    -        candidate = await manager.prepare_candidate("calendar")
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -        validation_root = candidate.validation_workspace
    -        assert validation_root is not None
    -
    -        process_host = manager._composition_generation_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[stable.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions and actions[0].phase == "degraded":
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -        assert len(manager._reload_journal.pending_recovery()) == 1  # pyright: ignore[reportPrivateUsage]
    -        assert action.tx_id == candidate.reload_tx_id
    -        assert action.generation_id == candidate.generation_id
    -        assert action.base_generation_id == stable.generation_id
    -        assert action.recovery_target == "base"
    -        with pytest.raises(RuntimeError, match="撤销准入"):
    -            await manager.publish_prepared("calendar")
    -
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -
    -        assert recovered["recovery_target"] == "base"
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.generation("calendar") is stable
    -        assert candidate.scope.closed
    -        assert not validation_root.parent.exists()
    -        assert _port_live(18000)
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "recovered"
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_watchdog_failure_revokes_ready_candidate_and_restores_base(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-watchdog-ready")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, latest_artifact)
    -    _upgrade_static_manager_plugin(latest_artifact, "2")
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.generation("calendar@lab")
    -        stable_snapshot = manager.current_snapshot
    -        assert stable is not None and stable_snapshot is not None
    -        write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -        result = (await manager.reconcile_changed())[0]
    -        ready = manager.ready_candidate
    -        assert result.get("publication_state") == "latest_ready"
    -        assert ready is not None and ready.reload_tx_id is not None
    -        candidate_snapshot = ready.runtime_snapshot
    -        assert candidate_snapshot is not None
    -        candidate_runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            ready.generation_id
    -        )
    -        assert candidate_runtime is not None and candidate_runtime.processes is not None
    -        candidate_port = candidate_runtime.processes.endpoint("calendar_api").port
    -
    -        process_host = manager._composition_generation_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[stable.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions and actions[0].phase == "degraded":
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -        assert len(manager._reload_journal.pending_recovery()) == 1  # pyright: ignore[reportPrivateUsage]
    -        assert action.tx_id == ready.reload_tx_id
    -        assert action.recovery_target == "base"
    -        assert not candidate_snapshot.accepting_leases
    -        with pytest.raises(RuntimeError, match="撤销准入"):
    -            await manager.switch_ready("calendar@lab")
    -        pointers = read_pointers(plugin_base)
    -        assert pointers is not None
    -        assert pointers.stable == stable_pointer
    -        assert pointers.latest == latest_pointer
    -        manager._reload_journal.advance(  # pyright: ignore[reportPrivateUsage]
    -            action.tx_id,
    -            "degraded",
    -            resource="plugin-skill-projection",
    -            error="formal skill rollback incomplete",
    -            recovery_target="base",
    -        )
    -
    -        recovered = await manager.retry_runtime_recovery("calendar@lab")
    -
    -        assert recovered["recovery_target"] == "base"
    -        assert "stable-skill-projection-restored" in str(
    -            recovered["retry_receipt"]
    -        )
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager.generation("calendar@lab") is stable
    -        assert manager.ready_candidate is None
    -        assert ready.scope.closed
    -        assert _port_live(18000)
    -        assert not _port_live(candidate_port)
    -        pointers = read_pointers(plugin_base)
    -        assert pointers is not None
    -        assert pointers.stable == stable_pointer
    -        assert pointers.latest == latest_pointer
    -        assert manager._reload_journal.get(action.tx_id).phase == "recovered"  # pyright: ignore[reportPrivateUsage]
    -    finally:
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_runtime_recovery_finishes_after_resume_even_when_caller_cancelled(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-recovery-cancel")
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    release = asyncio.Event()
    -    entered = asyncio.Event()
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("calendar")
    -        assert generation is not None
    -        process_host = manager._composition_generation_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[generation.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions:
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -
    -        async def blocking_resumer() -> None:
    -            entered.set()
    -            await release.wait()
    -
    -        manager._endpoint_resumer = blocking_resumer  # pyright: ignore[reportPrivateUsage]
    -        retry = asyncio.create_task(manager.retry_runtime_recovery("calendar"))
    -        await entered.wait()
    -        assert manager._reload_journal.get(action.tx_id).phase == "degraded"  # pyright: ignore[reportPrivateUsage]
    -        retry.cancel()
    -        await asyncio.sleep(0)
    -        assert not retry.done()
    -        release.set()
    -        with pytest.raises(asyncio.CancelledError):
    -            await retry
    -
    -        assert manager._reload_journal.get(action.tx_id).phase == "recovered"  # pyright: ignore[reportPrivateUsage]
    -        assert manager.current_snapshot is not None
    -        assert manager.current_snapshot.accepting_leases
    -    finally:
    -        release.set()
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_runtime_recovery_finishes_host_retry_before_exposing_cancellation(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-host-retry-cancel")
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    release = asyncio.Event()
    -    entered = asyncio.Event()
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("calendar")
    -        assert generation is not None
    -        composition_host = manager._composition_generation_host  # pyright: ignore[reportPrivateUsage]
    -        process_host = composition_host._process_host  # pyright: ignore[reportPrivateUsage]
    -        process_host._recovery_backoff_seconds = ()  # pyright: ignore[reportPrivateUsage]
    -        owned = process_host._generations[generation.generation_id]  # pyright: ignore[reportPrivateUsage]
    -        process = owned.entries["calendar_api"].process
    -        assert process is not None
    -        process.kill()
    -        action = None
    -        for _ in range(100):
    -            actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -            if actions:
    -                action = actions[0]
    -                break
    -            await asyncio.sleep(0.01)
    -        assert action is not None
    -        original_retry = composition_host.retry_runtime_recovery
    -
    -        async def blocking_retry(generation_id: str) -> str:
    -            entered.set()
    -            await release.wait()
    -            return await original_retry(generation_id)
    -
    -        monkeypatch.setattr(composition_host, "retry_runtime_recovery", blocking_retry)
    -        retry = asyncio.create_task(manager.retry_runtime_recovery("calendar"))
    -        await entered.wait()
    -        retry.cancel()
    -        await asyncio.sleep(0)
    -        assert not retry.done()
    -        assert manager._reload_journal.get(action.tx_id).phase == "degraded"  # pyright: ignore[reportPrivateUsage]
    -
    -        release.set()
    -        with pytest.raises(asyncio.CancelledError):
    -            await retry
    -
    -        assert manager._reload_journal.get(action.tx_id).phase == "recovered"  # pyright: ignore[reportPrivateUsage]
    -        runtime = composition_host.get(generation.generation_id)
    -        assert runtime is not None and runtime.processes is not None
    -        assert _port_live(runtime.processes.endpoint("calendar_api").port)
    -    finally:
    -        release.set()
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_start_cleanup_failure_keeps_durable_retry_owner(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-candidate-start-failure")
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    original_terminate = OwnedProcessGroup.terminate
    -
    -    async def fail_runtime_cleanup(
    -        _group: OwnedProcessGroup,
    -        *,
    -        timeout_s: float,
    -    ) -> None:
    -        _ = timeout_s
    -        raise RuntimeError("injected candidate start cleanup failure")
    -
    -    try:
    -        await manager.load_all()
    -        stable_snapshot = manager.current_snapshot
    -        _upgrade_static_manager_plugin(plugin_dir, "2")
    -        candidate = await manager.prepare_candidate("calendar")
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -        (plugin_dir / "mcp.py").write_text(
    -            "raise SystemExit(23)\n",
    -            encoding="utf-8",
    -        )
    -        monkeypatch.setattr(
    -            OwnedProcessGroup,
    -            "terminate",
    -            fail_runtime_cleanup,
    -        )
    -
    -        with pytest.raises(RuntimeError, match="runtime cleanup 未完成"):
    -            await manager.publish_prepared("calendar")
    -
    -        record = manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        )
    -        assert record.phase == "cleanup_failed"
    -        assert record.recovery_action == "retry_generation_cleanup"
    -        assert record.recovery_target == "base"
    -        assert manager.current_snapshot is stable_snapshot
    -        assert manager._composition_generation_host.failure(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        ) is not None
    -        retained = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        )
    -        assert retained is not None and retained.processes is not None
    -        retained_port = retained.processes.endpoint("calendar_api").port
    -        assert _port_live(retained_port)
    -
    -        monkeypatch.setattr(
    -            OwnedProcessGroup,
    -            "terminate",
    -            original_terminate,
    -        )
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -
    -        assert recovered["recovery_target"] == "base"
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "aborted"
    -        assert not _port_live(retained_port)
    -    finally:
    -        monkeypatch.setattr(
    -            OwnedProcessGroup,
    -            "terminate",
    -            original_terminate,
    -        )
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_stable_boot_cleanup_failure_creates_durable_retry_owner(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-stable-start-failure")
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    (plugin_dir / "mcp.py").write_text(
    -        "raise SystemExit(24)\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    original_terminate = OwnedProcessGroup.terminate
    -
    -    async def fail_runtime_cleanup(
    -        _group: OwnedProcessGroup,
    -        *,
    -        timeout_s: float,
    -    ) -> None:
    -        _ = timeout_s
    -        raise RuntimeError("injected stable boot cleanup failure")
    -
    -    monkeypatch.setattr(OwnedProcessGroup, "terminate", fail_runtime_cleanup)
    -    try:
    -        with pytest.raises(RuntimeError, match="cleanup"):
    -            await manager.load_all()
    -
    -        actions = manager._reload_journal.pending_recovery()  # pyright: ignore[reportPrivateUsage]
    -        assert len(actions) == 1
    -        action = actions[0]
    -        assert action.plugin_id == "calendar"
    -        assert action.phase == "cleanup_failed"
    -        assert action.action == "retry_generation_cleanup"
    -        assert action.recovery_target == "base"
    -        assert action.runtime_owner_boot_id == "boot-stable-start-failure"
    -        retained = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            action.generation_id
    -        )
    -        assert retained is not None and retained.processes is not None
    -        retained_port = retained.processes.endpoint("calendar_api").port
    -        assert _port_live(retained_port)
    -
    -        monkeypatch.setattr(
    -            OwnedProcessGroup,
    -            "terminate",
    -            original_terminate,
    -        )
    -        recovered = await manager.retry_runtime_recovery("calendar")
    -
    -        assert recovered["recovery_target"] == "base"
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            action.tx_id
    -        ).phase == "aborted"
    -        assert not _port_live(retained_port)
    -    finally:
    -        monkeypatch.setattr(
    -            OwnedProcessGroup,
    -            "terminate",
    -            original_terminate,
    -        )
    -        await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_cleanup_failure_requires_host_retry_before_abort(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-cleanup-retry")
    -    monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    candidate_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, candidate_artifact)
    -    (candidate_artifact / "entry.py").write_text(
    -        _plugin_source("2"),
    -        encoding="utf-8",
    -    )
    -    candidate_manifest = candidate_artifact / "akashic.plugin.toml"
    -    candidate_manifest.write_text(
    -        candidate_manifest.read_text(encoding="utf-8")
    -        .replace('version = "1"', 'version = "2"')
    -        .replace('VERSION = "1"', 'VERSION = "2"'),
    -        encoding="utf-8",
    -    )
    -    base_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    candidate_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(plugin_base, stable=base_pointer, latest=base_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    original_terminate = OwnedProcessGroup.terminate
    -    terminate_calls = 0
    -
    -    async def fail_terminate_once(
    -        group: OwnedProcessGroup,
    -        *,
    -        timeout_s: float,
    -    ) -> None:
    -        nonlocal terminate_calls
    -        terminate_calls += 1
    -        if terminate_calls == 2:
    -            raise RuntimeError("injected process-group cleanup failure")
    -        await original_terminate(group, timeout_s=timeout_s)
    -
    -    try:
    -        await manager.load_all()
    -        stable_snapshot = manager.current_snapshot
    -        assert stable_snapshot is not None and _port_live(18000)
    -        write_pointers(
    -            plugin_base,
    -            stable=base_pointer,
    -            latest=candidate_pointer,
    -        )
    -        result = (await manager.reconcile_changed())[0]
    -        candidate = manager.ready_candidate
    -        assert result.get("publication_state") == "latest_ready"
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -        runtime = manager._composition_generation_host.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        )
    -        assert runtime is not None and runtime.processes is not None
    -        candidate_port = runtime.processes.endpoint("calendar_api").port
    -        monkeypatch.setattr(OwnedProcessGroup, "terminate", fail_terminate_once)
    -
    -        with pytest.raises(RuntimeError, match="runtime cleanup 未完成"):
    -            await manager.drop_candidate("calendar@lab")
    -
    -        record = manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        )
    -        assert record.phase == "cleanup_failed"
    -        assert record.recovery_target == "base"
    -        assert manager.ready_candidate is candidate
    -        assert manager.current_snapshot is stable_snapshot
    -        assert _port_live(18000)
    -        assert manager._composition_generation_host.failure(  # pyright: ignore[reportPrivateUsage]
    -            candidate.generation_id
    -        ) is not None
    -
    -        recovered = await manager.retry_runtime_recovery("calendar@lab")
    -
    -        assert recovered["publication_state"] == "recovered"
    -        assert recovered["recovery_target"] == "base"
    -        assert manager.ready_candidate is None
    -        assert manager.current_snapshot is stable_snapshot
    -        assert _port_live(18000) and not _port_live(candidate_port)
    -        assert manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "aborted"
    -    finally:
    -        monkeypatch.setattr(OwnedProcessGroup, "terminate", original_terminate)
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_stable_boot_cleanup_failure_targets_base(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-installed-stable-failure")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    shutil.copytree(source_root, stable_artifact)
    -    (stable_artifact / "mcp.py").write_text(
    -        "raise SystemExit(24)\n",
    -        encoding="utf-8",
    -    )
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    original_terminate = OwnedProcessGroup.terminate
    -
    -    async def fail_runtime_cleanup(
    -        _group: OwnedProcessGroup,
    -        *,
    -        timeout_s: float,
    -    ) -> None:
    -        _ = timeout_s
    -        raise RuntimeError("injected installed stable cleanup failure")
    -
    -    monkeypatch.setattr(OwnedProcessGroup, "terminate", fail_runtime_cleanup)
    -    try:
    -        with pytest.raises(RuntimeError, match="cleanup"):
    -            await manager.load_all()
    -        action = manager._reload_journal.pending_recovery()[0]  # pyright: ignore[reportPrivateUsage]
    -        assert action.action == "retry_generation_cleanup"
    -        assert action.recovery_target == "base"
    -        assert action.base_artifact_pointer == stable_pointer.path
    -        assert action.candidate_artifact_pointer is None
    -        assert manager.current_snapshot is None
    -
    -        monkeypatch.setattr(OwnedProcessGroup, "terminate", original_terminate)
    -        recovered = await manager.retry_runtime_recovery("calendar@lab")
    -
    -        assert recovered["recovery_target"] == "base"
    -        assert recovered["generation_id"] is None
    -        assert recovered["snapshot_id"] is None
    -        assert manager._reload_journal.get(action.tx_id).phase == "aborted"  # pyright: ignore[reportPrivateUsage]
    -        pointers = read_pointers(plugin_base)
    -        assert pointers is not None
    -        assert pointers.stable == stable_pointer
    -        assert pointers.latest == stable_pointer
    -    finally:
    -        monkeypatch.setattr(OwnedProcessGroup, "terminate", original_terminate)
    -        await manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_supervised_boot_reconciles_degraded_runtime_to_exact_base(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-runtime-old")
    -    monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    latest_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, latest_artifact)
    -    (latest_artifact / "entry.py").write_text(
    -        _plugin_source("2"),
    -        encoding="utf-8",
    -    )
    -    latest_manifest = latest_artifact / "akashic.plugin.toml"
    -    latest_manifest.write_text(
    -        latest_manifest.read_text(encoding="utf-8")
    -        .replace('version = "1"', 'version = "2"')
    -        .replace('VERSION = "1"', 'VERSION = "2"'),
    -        encoding="utf-8",
    -    )
    -    stable_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    latest_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(plugin_base, stable=stable_pointer, latest=stable_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    old_manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    new_manager: PluginManager | None = None
    -    orphan: asyncio.subprocess.Process | None = None
    -    try:
    -        await old_manager.load_all()
    -        write_pointers(plugin_base, stable=stable_pointer, latest=latest_pointer)
    -        _ = await old_manager.reconcile_changed()
    -        candidate = old_manager.ready_candidate
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -
    -        original_start = old_manager._composition_generation_host.start  # pyright: ignore[reportPrivateUsage]
    -        formal_failed = False
    -
    -        async def fail_formal_once(*args, **kwargs):
    -            nonlocal formal_failed
    -            if kwargs.get("mode") == "formal" and not formal_failed:
    -                formal_failed = True
    -                raise RuntimeError("formal start failed before pointer commit")
    -            return await original_start(*args, **kwargs)
    -
    -        async def fail_restore(*_args, **_kwargs):
    -            raise RuntimeError("old runtime restore remains uncertain")
    -
    -        monkeypatch.setattr(
    -            old_manager._composition_generation_host,  # pyright: ignore[reportPrivateUsage]
    -            "start",
    -            fail_formal_once,
    -        )
    -        monkeypatch.setattr(
    -            old_manager,
    -            "_recover_stable_root",
    -            fail_restore,
    -        )
    -        with pytest.raises(RuntimeError, match="candidate formalization 失败"):
    -            await old_manager.switch_ready("calendar@lab")
    -        action = old_manager._reload_journal.pending_recovery()[0]  # pyright: ignore[reportPrivateUsage]
    -        assert action.phase == "degraded"
    -        assert action.recovery_target == "base"
    -        assert action.runtime_owner_boot_id == "boot-runtime-old"
    -
    -        orphan_env = dict(os.environ)
    -        orphan_env["AKASHIC_BOOT_ID"] = "boot-runtime-old"
    -        orphan = await asyncio.create_subprocess_exec(
    -            sys.executable,
    -            "-c",
    -            "import time; time.sleep(60)",
    -            env=orphan_env,
    -            start_new_session=True,
    -        )
    -        assert orphan.returncode is None
    -
    -        monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-runtime-new")
    -        new_manager = _manager(
    -            tmp_path,
    -            tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -        )
    -        await new_manager.load_all()
    -
    -        pointers = read_pointers(plugin_base)
    -        assert pointers is not None
    -        assert pointers.stable == stable_pointer
    -        assert pointers.latest == latest_pointer
    -        assert await asyncio.wait_for(orphan.wait(), timeout=2) != 0
    -        stable = new_manager.generation("calendar@lab")
    -        snapshot = new_manager.current_snapshot
    -        assert stable is not None and snapshot is not None
    -        assert stable.plugin_dir == stable_artifact
    -        assert _port_live(18000)
    -        tool = snapshot.tool_registry.get_tool(  # type: ignore[union-attr]
    -            "mcp_calendar__get_events"
    -        )
    -        assert tool is not None
    -        assert await tool.execute() == "|".join(
    -            ("formal", "18000", str(stable.data_dir))
    -        )
    -        assert new_manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            action.tx_id
    -        ).phase == "recovered"
    -    finally:
    -        if orphan is not None and orphan.returncode is None:
    -            orphan.kill()
    -            _ = await orphan.wait()
    -        if new_manager is not None:
    -            await new_manager.terminate_all()
    -        await old_manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_supervised_boot_rebuilds_exact_candidate_after_pointer_commit(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-candidate-old")
    -    monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    candidate_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, candidate_artifact)
    -    (candidate_artifact / "entry.py").write_text(
    -        _plugin_source("2"),
    -        encoding="utf-8",
    -    )
    -    candidate_manifest = candidate_artifact / "akashic.plugin.toml"
    -    candidate_manifest.write_text(
    -        candidate_manifest.read_text(encoding="utf-8")
    -        .replace('version = "1"', 'version = "2"')
    -        .replace('VERSION = "1"', 'VERSION = "2"'),
    -        encoding="utf-8",
    -    )
    -    base_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    candidate_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(plugin_base, stable=base_pointer, latest=base_pointer)
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    old_manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    new_manager: PluginManager | None = None
    -    orphan: asyncio.subprocess.Process | None = None
    -    old_manager_closed = False
    -    try:
    -        await old_manager.load_all()
    -        write_pointers(
    -            plugin_base,
    -            stable=base_pointer,
    -            latest=candidate_pointer,
    -        )
    -        _ = await old_manager.reconcile_changed()
    -        candidate = old_manager.ready_candidate
    -        assert candidate is not None and candidate.reload_tx_id is not None
    -        old_manager._advance_reload(candidate, "promoting")  # pyright: ignore[reportPrivateUsage]
    -        write_pointers(
    -            plugin_base,
    -            stable=candidate_pointer,
    -            latest=candidate_pointer,
    -        )
    -        old_manager._advance_reload(  # pyright: ignore[reportPrivateUsage]
    -            candidate,
    -            "degraded",
    -            error="process crashed after pointer commit",
    -            resource=f"composition-runtime:{candidate.generation_id}",
    -            formal_effects=("candidate_pointer_committed",),
    -            recovery_action="retry_runtime_recovery",
    -            recovery_target="candidate",
    -        )
    -
    -        # A real new boot cannot coexist with the old Gateway event loop.
    -        await old_manager.terminate_all()
    -        old_manager_closed = True
    -        orphan_env = dict(os.environ)
    -        orphan_env["AKASHIC_BOOT_ID"] = "boot-candidate-old"
    -        orphan = await asyncio.create_subprocess_exec(
    -            sys.executable,
    -            "-c",
    -            "import time; time.sleep(60)",
    -            env=orphan_env,
    -            start_new_session=True,
    -        )
    -        assert orphan.returncode is None
    -
    -        monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-candidate-new")
    -        new_manager = _manager(
    -            tmp_path,
    -            tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -        )
    -        await new_manager.load_all()
    -
    -        pointers = read_pointers(plugin_base)
    -        assert pointers is not None
    -        assert pointers.stable == candidate_pointer
    -        assert pointers.latest == candidate_pointer
    -        stable = new_manager.generation("calendar@lab")
    -        snapshot = new_manager.current_snapshot
    -        assert stable is not None and snapshot is not None
    -        assert stable.plugin_dir == candidate_artifact
    -        assert stable.source_revision == candidate.source_revision
    -        tool = snapshot.tool_registry.get_tool(  # type: ignore[union-attr]
    -            "mcp_calendar__get_events"
    -        )
    -        assert tool is not None
    -        assert await tool.execute() == "|".join(
    -            ("formal", "18000", str(stable.data_dir))
    -        )
    -        assert new_manager._reload_journal.get(  # pyright: ignore[reportPrivateUsage]
    -            candidate.reload_tx_id
    -        ).phase == "recovered"
    -        assert await asyncio.wait_for(orphan.wait(), timeout=2) != 0
    -    finally:
    -        if orphan is not None and orphan.returncode is None:
    -            orphan.kill()
    -            _ = await orphan.wait()
    -        if new_manager is not None:
    -            await new_manager.terminate_all()
    -        if not old_manager_closed:
    -            await old_manager.terminate_all()
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.parametrize(
    -    ("supervised", "current_boot", "recorded_boot", "message"),
    -    (
    -        (False, "boot-new", "boot-old", "supervised boot identity"),
    -        (True, "boot-same", "boot-same", "不同于当前进程"),
    -        (True, "boot-new", None, "旧 boot identity"),
    -    ),
    -)
    -@pytest.mark.asyncio
    -async def test_runtime_recovery_rejects_unowned_boot_cleanup_without_pointer_write(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    supervised: bool,
    -    current_boot: str,
    -    recorded_boot: str | None,
    -    message: str,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", current_boot)
    -    if supervised:
    -        monkeypatch.setenv("AKASHIC_SUPERVISED", "1")
    -    else:
    -        monkeypatch.delenv("AKASHIC_SUPERVISED", raising=False)
    -    source_root = _write_static_manager_plugin(tmp_path / "source", "1")
    -    plugin_base = tmp_path / "home" / "cache" / "lab" / "calendar"
    -    stable_artifact = plugin_base / ".artifacts" / "1.0.0-stable"
    -    candidate_artifact = plugin_base / ".artifacts" / "2.0.0-latest"
    -    shutil.copytree(source_root, stable_artifact)
    -    shutil.copytree(source_root, candidate_artifact)
    -    base_pointer = ArtifactPointer(".artifacts/1.0.0-stable")
    -    candidate_pointer = ArtifactPointer(".artifacts/2.0.0-latest")
    -    write_pointers(
    -        plugin_base,
    -        stable=base_pointer,
    -        latest=candidate_pointer,
    -    )
    -    write_plugin_manifest(
    -        {"calendar@lab": True},
    -        plugins_home=tmp_path / "home",
    -    )
    -    manager = _manager(
    -        tmp_path,
    -        tool_registry=ToolRegistry(follow_runtime_snapshot=False),
    -    )
    -    tx_id = manager._reload_journal.begin(  # pyright: ignore[reportPrivateUsage]
    -        plugin_id="calendar@lab",
    -        base_snapshot_id="stable-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -        base_artifact_pointer=base_pointer.path,
    -        candidate_artifact_pointer=candidate_pointer.path,
    -    )
    -    if recorded_boot is not None:
    -        manager._reload_journal.mark_runtime_owner(  # pyright: ignore[reportPrivateUsage]
    -            tx_id,
    -            recorded_boot,
    -        )
    -    manager._reload_journal.advance(  # pyright: ignore[reportPrivateUsage]
    -        tx_id,
    -        "degraded",
    -        resource="composition-runtime:calendar:candidate:2",
    -        error="runtime owner uncertain",
    -        recovery_target="base",
    -    )
    -
    -    with pytest.raises(RuntimeError, match=message):
    -        await manager.load_all()
    -
    -    pointers = read_pointers(plugin_base)
    -    assert pointers is not None
    -    assert pointers.stable == base_pointer
    -    assert pointers.latest == candidate_pointer
    -    assert manager.current_snapshot is None
    -    assert manager.generation("calendar@lab") is None
    -    assert not _port_live(18000)
    -
    -
    -@pytest.mark.asyncio
    -async def test_static_candidate_without_formal_data_leaves_no_formal_directory(
    -    tmp_path: Path,
    -) -> None:
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    formal_data = tmp_path / "workspace" / "plugin-data" / "calendar-builtin"
    -
    -    candidate = await manager.prepare_candidate("calendar")
    -
    -    assert candidate is not None
    -    assert candidate.validation_workspace is not None
    -    assert candidate.runtime_snapshot is not None
    -    candidate_root = candidate.runtime_snapshot.composition_root
    -    assert candidate_root is not None
    -    runtime = candidate_root.root_fiber.children[0].runtime
    -    assert runtime is not None and runtime.data_dir.is_dir()
    -    assert candidate.validation_data_inventory == ()
    -    assert not formal_data.exists()
    -
    -    await manager.discard_prepared("calendar")
    -    assert not formal_data.exists()
    -    assert not candidate.validation_workspace.parent.exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_static_first_publication_rolls_back_new_formal_data_on_commit_failure(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    _ = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    formal_data = tmp_path / "workspace" / "plugin-data" / "calendar-builtin"
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None
    -
    -    def fail_owner_commit(*_args: object) -> None:
    -        raise RuntimeError("owner commit failed")
    -
    -    monkeypatch.setattr(
    -        manager,
    -        "_activate_published_generation",
    -        fail_owner_commit,
    -    )
    -    with pytest.raises(RuntimeError, match="owner commit failed"):
    -        await manager.publish_prepared("calendar")
    -
    -    assert manager.current_snapshot is None
    -    assert not formal_data.exists()
    -    assert candidate.scope.closed is True
    -
    -    monkeypatch.undo()
    -    replacement = await manager.prepare_candidate("calendar")
    -    assert replacement is not None
    -    result = await manager.publish_prepared("calendar")
    -    assert result["publication_state"] == "committed"
    -    assert formal_data.is_dir()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_static_manifest_declarations_do_not_activate_inactive_plugin(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    entrypoint = plugin_dir / "entry.py"
    -    entrypoint.write_text(
    -        entrypoint.read_text(encoding="utf-8")
    -        + "\ndef is_active(services):\n"
    -        + "    return False\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert snapshot.composition_active_plugin_ids == frozenset()
    -    assert snapshot.mcp_server_registry is not None
    -    assert len(snapshot.mcp_server_registry) == 0
    -    assert snapshot.managed_process_registry is not None
    -    assert len(snapshot.managed_process_registry) == 0
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("old", "new", "message"),
    -    (
    -        ('required_tools = ["get_events"]', 'required_tools = ["other"]', "MCP 声明"),
    -        ("formal_port = 18000", "formal_port = 18001", "managed process 声明"),
    -    ),
    -)
    -async def test_static_manifest_runtime_drift_excludes_failed_stable_plugin(
    -    tmp_path: Path,
    -    old: str,
    -    new: str,
    -    message: str,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manifest = plugin_dir / "akashic.plugin.toml"
    -    manifest.write_text(
    -        manifest.read_text(encoding="utf-8").replace(old, new),
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is None
    -    assert manager._active_generations == {}  # pyright: ignore[reportPrivateUsage]
    -    gate = manager.latest_gate("calendar")
    -    assert gate is not None and gate.status == "failed"
    -    assert message in str(gate.checks[-1].evidence)
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_keeps_candidate_mcp_registry_private_until_publish(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None and stable.mcp_server_registry is not None
    -    stable_registry = stable.mcp_server_registry
    -    assert stable_registry["calendar"].definition.candidate_env["VERSION"] == "1"
    -
    -    _upgrade_static_manager_plugin(plugin_dir, "2")
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_registry = candidate.runtime_snapshot.mcp_server_registry
    -    assert candidate_registry is not None
    -    assert candidate_registry["calendar"].definition.candidate_env["VERSION"] == "2"
    -    assert manager.current_snapshot is stable
    -    assert manager.current_snapshot.mcp_server_registry is stable_registry
    -
    -    result = await manager.publish_prepared("calendar")
    -    assert result["publication_state"] == "committed"
    -    current = manager.current_snapshot
    -    assert current is not None and current.mcp_server_registry is not None
    -    assert current.mcp_server_registry is not candidate_registry
    -    assert current.mcp_server_registry["calendar"].definition.candidate_env["VERSION"] == "2"
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_rejects_mcp_registry_drift_before_publish(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_static_manager_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None and stable.mcp_server_registry is not None
    -    stable_registry = stable.mcp_server_registry
    -
    -    _upgrade_static_manager_plugin(plugin_dir, "2")
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -
    -    def replace_with_stable_registry(snapshot: RuntimeSnapshot) -> None:
    -        snapshot.mcp_server_registry = stable_registry
    -        snapshot.mcp_server_registry_identity = stable_registry.identity
    -
    -    async def release_validation(_snapshot: RuntimeSnapshot) -> None:
    -        return None
    -
    -    manager.bind_dashboard_preparer(
    -        replace_with_stable_registry,
    -        validation_releaser=release_validation,
    -    )
    -    with pytest.raises(RuntimeError, match="MCP registry"):
    -        await manager.publish_prepared("calendar")
    -    assert manager.current_snapshot is stable
    -    assert manager.current_snapshot.mcp_server_registry is stable_registry
    -
    -    await manager.discard_prepared("calendar")
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_rejects_mcp_endpoint_without_same_owner_process(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _plugin_dir(tmp_path / "plugins")
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import (\n"
    -        "    MCP_SERVERS, EndpointEnv, McpServerDefinition,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'calendar'\n"
    -        "version = '1'\n"
    -        "inject = (MCP_SERVERS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(MCP_SERVERS).register(\n"
    -        "        ctx, McpServerDefinition(\n"
    -        "            name='calendar', command=('python', 'mcp.py'),\n"
    -        "            endpoint_env=(EndpointEnv('PORT', 'calendar_api'),),\n"
    -        "        ),\n"
    -        "    )\n",
    -        encoding="utf-8",
    -    )
    -    manager = _manager(tmp_path)
    -
    -    await manager.load_all()
    -
    -    assert manager.generation("calendar") is None
    -    gate = manager.latest_gate("calendar")
    -    assert gate is not None and gate.status == "failed"
    -    assert "缺少同 owner managed process" in gate.failure_reason
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_models.py b/tests/test_plugin_composition_models.py
    deleted file mode 100644
    index 488efcd7b..000000000
    --- a/tests/test_plugin_composition_models.py
    +++ /dev/null
    @@ -1,180 +0,0 @@
    -from __future__ import annotations
    -
    -from dataclasses import fields, replace
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    CHAT_MODELS,
    -    EMBEDDINGS,
    -    EmbeddingSpaceDescriptor,
    -    MODEL_CATALOG,
    -    MODEL_DRIVERS,
    -    MODEL_SETTINGS,
    -    LLMResponse,
    -    ModelCatalogSnapshot,
    -    ModelContinuation,
    -    ModelRole,
    -    ModelUnavailableError,
    -    RateLimitError,
    -    AuthenticationError,
    -    ModelRequest,
    -)
    -from tests.model_plugin_fakes import build_test_chat_models
    -
    -
    -def test_model_services_have_stable_distinct_keys() -> None:
    -    assert {
    -        CHAT_MODELS.name,
    -        EMBEDDINGS.name,
    -        MODEL_CATALOG.name,
    -        MODEL_SETTINGS.name,
    -        MODEL_DRIVERS.name,
    -    } == {
    -        "models.chat.v1",
    -        "models.embeddings.v1",
    -        "models.catalog.v1",
    -        "models.settings.v1",
    -        "models.drivers.v1",
    -    }
    -
    -
    -def test_model_request_cannot_select_provider_transport_or_secret() -> None:
    -    names = {item.name for item in fields(ModelRequest)}
    -
    -    assert names == {
    -        "continuation",
    -        "disable_reasoning",
    -        "messages",
    -        "tools",
    -        "max_output_tokens",
    -        "system_prompt",
    -        "tool_choice",
    -        "prompt_cache_key",
    -        "on_delta",
    -    }
    -    assert names.isdisjoint(
    -        {
    -            "model",
    -            "provider",
    -            "base_url",
    -            "api_key",
    -            "extra_body",
    -            "transport",
    -        }
    -    )
    -
    -
    -def test_model_response_exposes_only_opaque_continuation() -> None:
    -    names = {item.name for item in fields(LLMResponse)}
    -
    -    assert "continuation" in names
    -    assert "provider_fields" not in names
    -
    -
    -def test_continuation_and_driver_config_are_deeply_immutable() -> None:
    -    from agent.plugin_composition import DriverConnectionDescriptor
    -
    -    source = {"nested": {"items": ["one"]}}
    -    descriptor = DriverConnectionDescriptor(
    -        connection_id="connection",
    -        name="Connection",
    -        driver_id="driver",
    -        endpoint="https://example.test",
    -        auth_identity="account",
    -        config=source,
    -    )
    -    continuation = ModelContinuation(binding_id="binding", payload=source)
    -
    -    source["nested"]["items"].append("two")  # type: ignore[index, union-attr]
    -    assert descriptor.config["nested"]["items"] == ("one",)  # type: ignore[index]
    -    assert continuation.payload["nested"]["items"] == ("one",)  # type: ignore[index]
    -    with pytest.raises(TypeError):
    -        descriptor.config["nested"]["new"] = True  # type: ignore[index]
    -
    -
    -def test_immutable_json_rejects_non_finite_numbers_and_cycles() -> None:
    -    with pytest.raises(ValueError, match="有限值"):
    -        ModelContinuation(binding_id="binding", payload={"value": float("nan")})
    -
    -    cycle: dict[str, object] = {}
    -    cycle["self"] = cycle
    -    with pytest.raises(ValueError, match="循环引用"):
    -        ModelContinuation(binding_id="binding", payload=cycle)
    -
    -
    -def test_catalog_snapshot_copies_role_bindings() -> None:
    -    bindings = {ModelRole.DEFAULT: "chat"}
    -    snapshot = ModelCatalogSnapshot(
    -        revision=1,
    -        connections=(),
    -        models=(),
    -        role_bindings=bindings,
    -        default_embedding_model_id=None,
    -    )
    -
    -    bindings[ModelRole.DEFAULT] = "changed"
    -    assert snapshot.role_bindings[ModelRole.DEFAULT] == "chat"
    -    with pytest.raises(TypeError):
    -        snapshot.role_bindings[ModelRole.DEFAULT] = "forbidden"  # type: ignore[index]
    -
    -
    -def test_model_errors_expose_retry_contract() -> None:
    -    assert RateLimitError.retryable is True
    -    assert AuthenticationError.retryable is False
    -    from agent.plugin_composition import RevisionConflictError
    -
    -    assert RevisionConflictError.retryable is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_chat_model_fake_preserves_role_and_rejects_foreign_continuation() -> None:
    -    class Provider:
    -        def __init__(self) -> None:
    -            self.calls = 0
    -
    -        async def chat(self, **kwargs: object) -> LLMResponse:
    -            del kwargs
    -            self.calls += 1
    -            return LLMResponse(content="unexpected")
    -
    -    provider = Provider()
    -    chat_models = build_test_chat_models(provider)
    -    async with chat_models.execution() as execution:
    -        assert execution.chat(ModelRole.FAST).descriptor.role is ModelRole.FAST
    -        assert execution.chat(ModelRole.VISION).descriptor.role is ModelRole.VISION
    -        model = execution.chat(ModelRole.DEFAULT)
    -        with pytest.raises(ModelUnavailableError):
    -            await model.complete(
    -                ModelRequest(
    -                    messages=(),
    -                    continuation=ModelContinuation(
    -                        binding_id="foreign-binding",
    -                        payload={},
    -                    ),
    -                )
    -            )
    -    assert provider.calls == 0
    -
    -
    -def test_embedding_identity_changes_with_connection_and_capabilities() -> None:
    -    base = EmbeddingSpaceDescriptor(
    -        plugin_snapshot_id="snapshot",
    -        model_revision=1,
    -        model_id="embedding",
    -        connection_id="connection",
    -        driver_id="driver",
    -        driver_contract_version="1",
    -        auth_identity="account",
    -        connection_fingerprint="endpoint-a",
    -        model="wire-model",
    -        dimensions=3,
    -        normalization="none",
    -        capability_digest="caps-a",
    -    )
    -    first = base
    -
    -    assert first.identity != replace(
    -        base, connection_fingerprint="endpoint-b"
    -    ).identity
    -    assert first.identity != replace(base, capability_digest="caps-b").identity
    diff --git a/tests/test_plugin_composition_process_slots.py b/tests/test_plugin_composition_process_slots.py
    deleted file mode 100644
    index 19e326355..000000000
    --- a/tests/test_plugin_composition_process_slots.py
    +++ /dev/null
    @@ -1,377 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -import sys
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    MANAGED_PROCESSES,
    -    CompositionRoot,
    -    ManagedProcessDefinition,
    -    PluginRuntime,
    -)
    -from agent.plugin_composition.process_slots import (
    -    PluginManagedProcesses,
    -    _freeze_plugin_managed_processes,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import RuntimeSnapshot
    -from bus.event_bus import EventBus
    -
    -
    -def _plugin_dir(root: Path, name: str = "calendar") -> Path:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "api.py").write_text(
    -        "import http.server\n"
    -        "import os\n"
    -        "class Handler(http.server.BaseHTTPRequestHandler):\n"
    -        "    def do_GET(self):\n"
    -        "        self.send_response(200)\n"
    -        "        self.end_headers()\n"
    -        "    def log_message(self, format, *args):\n"
    -        "        pass\n"
    -        "server = http.server.ThreadingHTTPServer(\n"
    -        "    ('127.0.0.1', int(os.environ['PORT'])),\n"
    -        "    Handler,\n"
    -        ")\n"
    -        "server.serve_forever()\n",
    -        encoding="utf-8",
    -    )
    -    return plugin_dir
    -
    -
    -def _runtime(plugin_dir: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id=plugin_dir.name,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _definition(*, port: int = 18000) -> ManagedProcessDefinition:
    -    return ManagedProcessDefinition(
    -        name="calendar_api",
    -        command=("python", "api.py"),
    -        env={"MODE": "calendar"},
    -        port_env="PORT",
    -        formal_port=port,
    -        readiness_path="/health",
    -        startup_timeout_seconds=15.0,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_process_registry_freezes_health_identity_and_cleanup(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("process-registry")
    -    processes = PluginManagedProcesses(root.instance_token)
    -    _ = await root.context.provide(MANAGED_PROCESSES, processes)
    -    plugin_dir = _plugin_dir(tmp_path)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(MANAGED_PROCESSES).register(ctx, _definition())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MANAGED_PROCESSES,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    registry = _freeze_plugin_managed_processes(
    -        processes,
    -        root.instance_token,
    -    )
    -    binding = registry["calendar_api"]
    -    assert binding.descriptor.owner == "calendar"
    -    assert binding.definition.env["MODE"] == "calendar"
    -    assert binding.is_live()
    -    assert not hasattr(processes, "freeze")
    -    incident = binding.incident_reporter(
    -        "process_readiness_failed",
    -        "calendar_api did not become ready",
    -    )
    -    assert incident.owner == "calendar"
    -    assert root.recent_incidents() == (incident,)
    -
    -    await fiber.dispose()
    -    assert not binding.is_live()
    -    assert _freeze_plugin_managed_processes(
    -        processes,
    -        root.instance_token,
    -    ) is registry
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_process_registry_identity_ignores_root_and_runtime_path(
    -    tmp_path: Path,
    -) -> None:
    -    identities: list[str] = []
    -    for suffix in ("candidate", "formal"):
    -        root = CompositionRoot(f"process-{suffix}")
    -        processes = PluginManagedProcesses(root.instance_token)
    -        _ = await root.context.provide(MANAGED_PROCESSES, processes)
    -        plugin_dir = _plugin_dir(tmp_path / suffix)
    -
    -        async def apply(ctx) -> None:
    -            await ctx.require(MANAGED_PROCESSES).register(ctx, _definition())
    -
    -        _ = await root.mount(
    -            apply,
    -            name="calendar",
    -            inject=(MANAGED_PROCESSES,),
    -            runtime=_runtime(plugin_dir),
    -        )
    -        identities.append(
    -            _freeze_plugin_managed_processes(
    -                processes,
    -                root.instance_token,
    -            ).identity
    -        )
    -        await root.dispose()
    -    assert identities[0] == identities[1]
    -
    -
    -@pytest.mark.asyncio
    -async def test_process_registry_rejects_invalid_endpoint_and_artifact_escape(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _plugin_dir(tmp_path)
    -    outside = tmp_path / "outside"
    -    outside.mkdir()
    -    (plugin_dir / "outside-link").symlink_to(outside, target_is_directory=True)
    -    definitions = (
    -        ManagedProcessDefinition(
    -            name="bad_port",
    -            command=("python", "api.py"),
    -            formal_port=0,
    -        ),
    -        ManagedProcessDefinition(
    -            name="bad_ready",
    -            command=("python", "api.py"),
    -            formal_port=18000,
    -            readiness_path="https://example.com/health",
    -        ),
    -        ManagedProcessDefinition(
    -            name="escaped",
    -            command=("python", "api.py"),
    -            cwd="outside-link",
    -            formal_port=18000,
    -        ),
    -    )
    -    for index, definition in enumerate(definitions):
    -        root = CompositionRoot(f"process-invalid-{index}")
    -        processes = PluginManagedProcesses(root.instance_token)
    -        _ = await root.context.provide(MANAGED_PROCESSES, processes)
    -
    -        async def apply(ctx, definition=definition) -> None:
    -            await ctx.require(MANAGED_PROCESSES).register(ctx, definition)
    -
    -        _ = await root.mount(
    -            apply,
    -            name="calendar",
    -            inject=(MANAGED_PROCESSES,),
    -            runtime=_runtime(plugin_dir),
    -        )
    -        assert not root.receipt().ready
    -        registry = _freeze_plugin_managed_processes(
    -            processes,
    -            root.instance_token,
    -        )
    -        assert len(registry) == 0
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_process_registry_rejects_context_from_another_root(
    -    tmp_path: Path,
    -) -> None:
    -    root_a = CompositionRoot("process-root-a")
    -    root_b = CompositionRoot("process-root-b")
    -    processes_a = PluginManagedProcesses(root_a.instance_token)
    -    processes_b = PluginManagedProcesses(root_b.instance_token)
    -    _ = await root_a.context.provide(MANAGED_PROCESSES, processes_a)
    -    _ = await root_b.context.provide(MANAGED_PROCESSES, processes_b)
    -    plugin_dir = _plugin_dir(tmp_path)
    -
    -    async def apply(ctx) -> None:
    -        await processes_a.register(ctx, _definition())
    -
    -    _ = await root_b.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MANAGED_PROCESSES,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -
    -    assert any(
    -        "插件 managed process Service 不属于当前 Root" in (fiber.error or "")
    -        for fiber in root_b.receipt().fibers
    -    )
    -    assert root_a.receipt().health == ()
    -    assert root_b.receipt().health == ()
    -    assert root_a.receipt().effects == (
    -        "root:service:core.managed_processes",
    -    )
    -    assert root_b.receipt().effects == (
    -        "root:service:core.managed_processes",
    -    )
    -    assert (
    -        len(
    -            _freeze_plugin_managed_processes(
    -                processes_a,
    -                root_a.instance_token,
    -            )
    -        )
    -        == 0
    -    )
    -    assert (
    -        len(
    -            _freeze_plugin_managed_processes(
    -                processes_b,
    -                root_b.instance_token,
    -            )
    -        )
    -        == 0
    -    )
    -
    -    await root_b.dispose()
    -    await root_a.dispose()
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -def _source(version: str) -> str:
    -    return (
    -        "from agent.plugin_composition import (\n"
    -        "    MANAGED_PROCESSES, ManagedProcessDefinition,\n"
    -        ")\n"
    -        "api_version = 3\n"
    -        "name = 'calendar'\n"
    -        f"version = '{version}'\n"
    -        "inject = (MANAGED_PROCESSES,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(MANAGED_PROCESSES).register(\n"
    -        "        ctx, ManagedProcessDefinition(\n"
    -        "            name='calendar_api', command=('python', 'api.py'),\n"
    -        f"            env={{'VERSION': '{version}'}}, formal_port=18000,\n"
    -        "            readiness_path='/health',\n"
    -        "        ),\n"
    -        "    )\n"
    -    )
    -
    -
    -def _write_plugin(tmp_path: Path, version: str) -> Path:
    -    plugin_dir = _plugin_dir(tmp_path / "plugins")
    -    _write_plugin_version(plugin_dir, version)
    -    requirements = plugin_dir / "requirements.txt"
    -    requirements.write_text("", encoding="utf-8")
    -    interpreter = plugin_dir / ".venv" / "bin" / "python"
    -    interpreter.parent.mkdir(parents=True)
    -    interpreter.write_text(
    -        f"#!/bin/sh\nexec {sys.executable} \"$@\"\n",
    -        encoding="utf-8",
    -    )
    -    interpreter.chmod(0o755)
    -    return plugin_dir
    -
    -
    -def _write_plugin_version(plugin_dir: Path, version: str) -> None:
    -    (plugin_dir / "plugin.py").write_text(_source(version), encoding="utf-8")
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        'name = "calendar"\n'
    -        f'version = "{version}"\n'
    -        "api_version = 3\n"
    -        'entrypoint = "plugin.py"\n\n'
    -        "[[python]]\n"
    -        'requirements = "requirements.txt"\n\n'
    -        "[[processes]]\n"
    -        'name = "calendar_api"\n'
    -        'command = ["python", "api.py"]\n'
    -        f'env = {{VERSION = "{version}"}}\n'
    -        'port_env = "PORT"\n'
    -        "formal_port = 18000\n"
    -        'readiness_path = "/health"\n',
    -        encoding="utf-8",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_keeps_process_registry_private_and_rebuilds_formal(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None and stable.managed_process_registry is not None
    -    stable_registry = stable.managed_process_registry
    -    assert stable_registry["calendar_api"].definition.env["VERSION"] == "1"
    -
    -    _write_plugin_version(plugin_dir, "2")
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_registry = candidate.runtime_snapshot.managed_process_registry
    -    assert candidate_registry is not None
    -    assert candidate_registry["calendar_api"].definition.env["VERSION"] == "2"
    -    assert manager.current_snapshot is stable
    -    assert manager.current_snapshot.managed_process_registry is stable_registry
    -
    -    result = await manager.publish_prepared("calendar")
    -    assert result["publication_state"] == "committed"
    -    current = manager.current_snapshot
    -    assert current is not None and current.managed_process_registry is not None
    -    assert current.managed_process_registry is not candidate_registry
    -    assert current.managed_process_registry["calendar_api"].definition.env[
    -        "VERSION"
    -    ] == "2"
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_rejects_process_registry_from_another_root(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path, "1")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None and stable.managed_process_registry is not None
    -    stable_registry = stable.managed_process_registry
    -
    -    _write_plugin_version(plugin_dir, "2")
    -    candidate = await manager.prepare_candidate("calendar")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -
    -    def replace_registry(snapshot: RuntimeSnapshot) -> None:
    -        snapshot.managed_process_registry = stable_registry
    -        snapshot.managed_process_registry_identity = stable_registry.identity
    -
    -    async def release_validation(_snapshot: RuntimeSnapshot) -> None:
    -        return None
    -
    -    manager.bind_dashboard_preparer(
    -        replace_registry,
    -        validation_releaser=release_validation,
    -    )
    -    with pytest.raises(RuntimeError, match="managed process registry"):
    -        await manager.publish_prepared("calendar")
    -    assert manager.current_snapshot is stable
    -
    -    await manager.discard_prepared("calendar")
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_session_read.py b/tests/test_plugin_composition_session_read.py
    deleted file mode 100644
    index 15fbedce7..000000000
    --- a/tests/test_plugin_composition_session_read.py
    +++ /dev/null
    @@ -1,192 +0,0 @@
    -from __future__ import annotations
    -
    -import hashlib
    -from pathlib import Path
    -from types import MappingProxyType, SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugin_composition import SessionReadService, SessionReadSnapshot
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -from session.manager import SessionManager
    -
    -
    -def _database_snapshot(workspace: Path) -> dict[str, tuple[int, str]]:
    -    return {
    -        path.name: (path.stat().st_size, hashlib.sha256(path.read_bytes()).hexdigest())
    -        for path in workspace.glob("sessions.db*")
    -        if path.is_file()
    -    }
    -
    -
    -def test_session_read_returns_detached_existing_snapshot() -> None:
    -    messages: list[dict[str, object]] = [
    -        {
    -            "role": "user",
    -            "content": [{"type": "text", "text": "original"}],
    -        }
    -    ]
    -    session = SimpleNamespace(messages=messages, last_consolidated=1)
    -    compaction = SimpleNamespace(generation=1, consolidated_through_seq=1)
    -    service = SessionReadService(cast(Any, lambda _key: (session, compaction)))
    -
    -    snapshot = service.read("mobile:one")
    -
    -    assert snapshot is not None
    -    assert snapshot.session_key == "mobile:one"
    -    assert snapshot.compaction_generation == 1
    -    assert snapshot.consolidated_through_seq == 1
    -    assert isinstance(snapshot.messages[0], MappingProxyType)
    -    messages[0]["role"] = "assistant"
    -    nested = cast(list[dict[str, object]], snapshot.messages[0]["content"])
    -    nested[0]["text"] = "snapshot-only"
    -    assert snapshot.messages[0]["role"] == "user"
    -    original = cast(list[dict[str, object]], messages[0]["content"])
    -    assert original[0]["text"] == "original"
    -
    -
    -def test_session_read_missing_and_candidate_boundaries_fail_loud() -> None:
    -    calls: list[str] = []
    -
    -    def missing(session_key: str):
    -        calls.append(session_key)
    -        raise KeyError(session_key)
    -
    -    formal = SessionReadService(missing)
    -    candidate = SessionReadService.candidate_validation()
    -    assert formal.formal is True
    -    assert candidate.formal is False
    -    assert formal.read("mobile:missing") is None
    -    assert calls == ["mobile:missing"]
    -    with pytest.raises(RuntimeError, match="candidate 验证期禁止"):
    -        candidate.read("mobile:existing")
    -
    -    inconsistent = SimpleNamespace(messages=[], last_consolidated=2)
    -    active = SimpleNamespace(generation=1, consolidated_through_seq=3)
    -    with pytest.raises(RuntimeError, match="active compaction generation 不一致"):
    -        SessionReadService(cast(Any, lambda _key: (inconsistent, active))).read(
    -            "mobile:inconsistent"
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_injects_formal_session_read_without_persistence_write(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    session = sessions.get_or_create("mobile:existing")
    -    session.messages = [
    -        {"role": "user", "content": "hello"},
    -        {"role": "assistant", "content": "hi"},
    -    ]
    -    sessions.save(session)
    -    sessions.invalidate(session.key)
    -    before = _database_snapshot(workspace)
    -    plugin_dir = tmp_path / "plugins" / "session_read_probe"
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import SESSION_READ\n"
    -        "api_version = 3\n"
    -        "name = 'session_read_probe'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (SESSION_READ,)\n"
    -        "snapshot = None\n"
    -        "async def apply(ctx, config):\n"
    -        "    global snapshot\n"
    -        "    snapshot = ctx.require(SESSION_READ).read('mobile:existing')\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -    try:
    -        await manager.load_all()
    -
    -        generation = manager.generation("session_read_probe")
    -        current = manager.current_snapshot
    -        assert generation is not None and current is not None
    -        assert isinstance(generation.instance, ComposablePlugin)
    -        snapshot = cast(SessionReadSnapshot, generation.instance.module.snapshot)
    -        assert tuple(message["role"] for message in snapshot.messages) == (
    -            "user",
    -            "assistant",
    -        )
    -        assert snapshot.compaction_generation is None
    -        assert snapshot.consolidated_through_seq is None
    -        assert _database_snapshot(workspace) == before
    -        assert current.composition_topology is not None
    -        assert "core.session_read" in current.composition_topology.services
    -
    -        root = current.composition_root
    -        assert root is not None
    -        await manager.terminate_all()
    -        assert root.receipt().services == ()
    -        assert root.receipt().effects == ()
    -    finally:
    -        sessions.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_session_read_fails_without_touching_stable_session(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    session = sessions.get_or_create("mobile:existing")
    -    session.messages = [{"role": "user", "content": "protected"}]
    -    sessions.save(session)
    -    plugin_dir = tmp_path / "plugins" / "session_read_probe"
    -    plugin_dir.mkdir(parents=True)
    -    plugin_path = plugin_dir / "plugin.py"
    -    plugin_path.write_text(
    -        "from agent.plugin_composition import SESSION_READ\n"
    -        "api_version = 3\n"
    -        "name = 'session_read_probe'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (SESSION_READ,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    ctx.require(SESSION_READ)\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.current_snapshot
    -        assert stable is not None
    -        before = _database_snapshot(workspace)
    -        plugin_path.write_text(
    -            "from agent.plugin_composition import SESSION_READ\n"
    -            "api_version = 3\n"
    -            "name = 'session_read_probe'\n"
    -            "version = '2.0.0'\n"
    -            "inject = (SESSION_READ,)\n"
    -            "async def apply(ctx, config):\n"
    -            "    ctx.require(SESSION_READ).read('mobile:existing')\n",
    -            encoding="utf-8",
    -        )
    -
    -        candidate = await manager.prepare_candidate("session_read_probe")
    -
    -        assert candidate is None
    -        assert manager.current_snapshot is stable
    -        assert manager.prepared_generation("session_read_probe") is None
    -        assert _database_snapshot(workspace) == before
    -    finally:
    -        await manager.terminate_all()
    -        sessions.close()
    diff --git a/tests/test_plugin_composition_tool_catalog.py b/tests/test_plugin_composition_tool_catalog.py
    deleted file mode 100644
    index f8b640d75..000000000
    --- a/tests/test_plugin_composition_tool_catalog.py
    +++ /dev/null
    @@ -1,844 +0,0 @@
    -from pathlib import Path
    -from types import MappingProxyType, SimpleNamespace
    -from typing import Any
    -
    -import pytest
    -
    -from agent.plugin_composition.context import CompositionRoot, PluginRuntime
    -from agent.plugin_composition.model import CompositionError, ServiceKey
    -from agent.plugin_composition.tool_catalog import (
    -    TOOL_CATALOG,
    -    PluginToolDefinition,
    -    PluginTools,
    -    _freeze_plugin_tools,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from agent.looping.core import _disable_candidate_side_effect_tools
    -from agent.tools.base import Tool
    -from agent.tools.registry import ToolRegistry
    -from bus.events import InboundMessage
    -from bus.event_bus import EventBus
    -
    -
    -def _definition(*, name: str = "inspect_repository") -> PluginToolDefinition:
    -    return PluginToolDefinition(
    -        name=name,
    -        description="Inspect one repository without changing it.",
    -        parameters={
    -            "type": "object",
    -            "properties": {
    -                "repository": {"type": "string"},
    -                "limit": {"type": "integer"},
    -            },
    -            "required": ["repository"],
    -            "additionalProperties": False,
    -        },
    -        handler_export="runtime.inspect_repository",
    -        risk="read-only",
    -    )
    -
    -
    -def _runtime(tmp_path: Path, plugin_id: str) -> PluginRuntime:
    -    plugin_dir = tmp_path / plugin_id
    -    plugin_dir.mkdir(exist_ok=True)
    -    return PluginRuntime(
    -        plugin_id=plugin_id,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -class _MarketplaceWriteTool(Tool):
    -    name = "marketplace_write"
    -    description = "Write through one marketplace plugin."
    -    parameters = {"type": "object", "properties": {}, "required": []}
    -
    -    async def execute(self, **kwargs: Any) -> str:
    -        del kwargs
    -        return "ok"
    -
    -
    -@pytest.mark.parametrize(
    -    "parameters",
    -    [
    -        {"type": "array", "items": {"type": "string"}},
    -        {
    -            "type": "object",
    -            "properties": {"repository": {"type": "string"}},
    -            "required": ["missing"],
    -            "additionalProperties": False,
    -        },
    -        {
    -            "type": "object",
    -            "properties": {},
    -            "required": [],
    -            "additionalProperties": True,
    -        },
    -    ],
    -)
    -def test_tool_definition_rejects_malformed_schema_before_registration(
    -    parameters: dict[str, object],
    -) -> None:
    -    with pytest.raises(ValueError):
    -        PluginToolDefinition(
    -            name="inspect_repository",
    -            description="Inspect one repository.",
    -            parameters=parameters,
    -            handler_export="runtime.inspect_repository",
    -        )
    -
    -
    -def test_tool_definition_rejects_non_string_schema_key() -> None:
    -    with pytest.raises(TypeError, match="object key 必须是字符串"):
    -        PluginToolDefinition(
    -            name="inspect_repository",
    -            description="Inspect one repository.",
    -            parameters={
    -                "type": "object",
    -                "properties": {1: {"type": "string"}},
    -                "required": [],
    -                "additionalProperties": False,
    -            },
    -            handler_export="runtime.inspect_repository",
    -        )
    -
    -
    -def test_candidate_side_effect_fence_uses_full_marketplace_plugin_id() -> None:
    -    registry = ToolRegistry(validate_semantic_schema=False)
    -    registry.register(
    -        _MarketplaceWriteTool(),
    -        risk="external-side-effect",
    -        source_type="plugin",
    -        source_name="watcher@github",
    -    )
    -    generation = SimpleNamespace(
    -        instance=SimpleNamespace(name="watcher"),
    -        contributions=SimpleNamespace(mcp_servers={}),
    -    )
    -    snapshot = SimpleNamespace(
    -        generations=MappingProxyType({"watcher@github": generation}),
    -        mcp_server_registry=None,
    -    )
    -    message = InboundMessage("web", "hua", "1", "inspect")
    -
    -    _disable_candidate_side_effect_tools(
    -        message,
    -        frozenset({"watcher@github"}),
    -        registry,
    -        snapshot,
    -    )
    -
    -    assert message.metadata["disabled_tools"] == ["marketplace_write"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_provided_service_resolves_its_exact_bound_tool(tmp_path: Path) -> None:
    -    capability = ServiceKey[object]("fixture.lookup.v1")
    -    root = CompositionRoot("provided-tool")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        _ = await ctx.provide(capability, object())
    -        await ctx.require(TOOL_CATALOG).register(
    -            ctx,
    -            _definition(name="fixture_lookup"),
    -            provided_for=capability,
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "fixture"),
    -    )
    -    _ = _freeze_plugin_tools(
    -        tools,
    -        root.instance_token,
    -        {"fixture": "fixture:generation"},
    -        root.plugin_service_owners(),
    -    )
    -
    -    assert tools.from_provide(capability) == "fixture_lookup"
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_empty_provided_marker_fails_loud_when_tool_is_requested(
    -    tmp_path: Path,
    -) -> None:
    -    marker = ServiceKey[object]("fixture.marker.v1")
    -    root = CompositionRoot("empty-provided-tool")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        _ = await ctx.provide(marker, object())
    -
    -    _ = await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "fixture"),
    -    )
    -    _ = _freeze_plugin_tools(
    -        tools,
    -        root.instance_token,
    -        {"fixture": "fixture:generation"},
    -        root.plugin_service_owners(),
    -    )
    -
    -    with pytest.raises(CompositionError) as raised:
    -        tools.from_provide(marker)
    -    assert raised.value.code == "PROVIDED_TOOL_NOT_BOUND"
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_bound_tool_requires_an_existing_provided_service(tmp_path: Path) -> None:
    -    capability = ServiceKey[object]("fixture.missing.v1")
    -    root = CompositionRoot("missing-provide")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(TOOL_CATALOG).register(
    -            ctx,
    -            _definition(name="missing_lookup"),
    -            provided_for=capability,
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="consumer",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "missing-consumer"),
    -    )
    -    with pytest.raises(CompositionError) as raised:
    -        _ = _freeze_plugin_tools(
    -            tools,
    -            root.instance_token,
    -            {"missing-consumer": "consumer:generation"},
    -            root.plugin_service_owners(),
    -        )
    -    assert raised.value.code == "PROVIDED_SERVICE_MISSING"
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_bound_tool_must_share_its_service_owner(tmp_path: Path) -> None:
    -    capability = ServiceKey[object]("fixture.owned.v1")
    -    root = CompositionRoot("owner-mismatch")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def provide(ctx) -> None:
    -        _ = await ctx.provide(capability, object())
    -
    -    async def bind(ctx) -> None:
    -        await ctx.require(TOOL_CATALOG).register(
    -            ctx,
    -            _definition(name="foreign_lookup"),
    -            provided_for=capability,
    -        )
    -
    -    _ = await root.mount(provide, name="provider", runtime=_runtime(tmp_path, "owner"))
    -    _ = await root.mount(
    -        bind,
    -        name="consumer",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "foreign-consumer"),
    -    )
    -    with pytest.raises(CompositionError) as raised:
    -        _ = _freeze_plugin_tools(
    -            tools,
    -            root.instance_token,
    -            {
    -                "owner": "provider:generation",
    -                "foreign-consumer": "consumer:generation",
    -            },
    -            root.plugin_service_owners(),
    -        )
    -    assert raised.value.code == "PROVIDED_TOOL_OWNER_MISMATCH"
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_one_provided_service_binds_at_most_one_tool(tmp_path: Path) -> None:
    -    capability = ServiceKey[object]("fixture.one-tool.v1")
    -    root = CompositionRoot("duplicate-provided-tool")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        _ = await ctx.provide(capability, object())
    -        for name in ("lookup_one", "lookup_two"):
    -            await ctx.require(TOOL_CATALOG).register(
    -                ctx,
    -                _definition(name=name),
    -                provided_for=capability,
    -            )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="provider",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "duplicate-provider"),
    -    )
    -    with pytest.raises(CompositionError) as raised:
    -        _ = _freeze_plugin_tools(
    -            tools,
    -            root.instance_token,
    -            {"duplicate-provider": "provider:generation"},
    -            root.plugin_service_owners(),
    -        )
    -    assert raised.value.code == "DUPLICATE_PROVIDED_TOOL"
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_provided_tool_resolution_uses_complete_active_snapshot(
    -    tmp_path: Path,
    -) -> None:
    -    capability = ServiceKey[object]("fixture.lookup.v1")
    -    provider_root = CompositionRoot("stable-provider")
    -    provider_tools = PluginTools(provider_root.instance_token)
    -    _ = await provider_root.context.provide(TOOL_CATALOG, provider_tools)
    -
    -    async def provide(ctx) -> None:
    -        _ = await ctx.provide(capability, object())
    -        await ctx.require(TOOL_CATALOG).register(
    -            ctx,
    -            _definition(name="stable_lookup"),
    -            provided_for=capability,
    -        )
    -
    -    _ = await provider_root.mount(
    -        provide,
    -        name="provider",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "provider"),
    -    )
    -    provider_catalog = _freeze_plugin_tools(
    -        provider_tools,
    -        provider_root.instance_token,
    -        {"provider": "provider:generation"},
    -        provider_root.plugin_service_owners(),
    -    )
    -
    -    candidate_root = CompositionRoot("candidate-consumer")
    -    candidate_tools = PluginTools(candidate_root.instance_token)
    -    _ = await candidate_root.context.provide(TOOL_CATALOG, candidate_tools)
    -    _ = _freeze_plugin_tools(
    -        candidate_tools,
    -        candidate_root.instance_token,
    -        {},
    -        candidate_root.plugin_service_owners(),
    -    )
    -    lease = SimpleNamespace(
    -        active=True,
    -        snapshot=SimpleNamespace(plugin_tool_catalog=provider_catalog),
    -    )
    -    token = bind_runtime_snapshot(lease)  # type: ignore[arg-type]
    -    try:
    -        assert candidate_tools.from_provide(capability) == "stable_lookup"
    -    finally:
    -        reset_runtime_snapshot(token)
    -    await candidate_root.dispose()
    -    await provider_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_provided_tool_resolution_uses_compiled_catalog_in_background(
    -    tmp_path: Path,
    -) -> None:
    -    capability = ServiceKey[object]("fixture.background_lookup.v1")
    -    provider_root = CompositionRoot("background-provider")
    -    provider_tools = PluginTools(provider_root.instance_token)
    -    _ = await provider_root.context.provide(TOOL_CATALOG, provider_tools)
    -
    -    async def provide(ctx) -> None:
    -        _ = await ctx.provide(capability, object())
    -        await ctx.require(TOOL_CATALOG).register(
    -            ctx,
    -            _definition(name="background_lookup"),
    -            provided_for=capability,
    -        )
    -
    -    _ = await provider_root.mount(
    -        provide,
    -        name="provider",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "background-provider"),
    -    )
    -    provider_catalog = _freeze_plugin_tools(
    -        provider_tools,
    -        provider_root.instance_token,
    -        {"background-provider": "provider:generation"},
    -        provider_root.plugin_service_owners(),
    -    )
    -    consumer_tools = PluginTools(object())
    -    consumer_tools._bind_runtime_catalog(provider_catalog)
    -
    -    assert consumer_tools.from_provide(capability) == "background_lookup"
    -    await provider_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_tool_catalog_identity_is_content_based_and_root_local(
    -    tmp_path: Path,
    -) -> None:
    -    async def build(root_name: str):
    -        root = CompositionRoot(root_name)
    -        tools = PluginTools(root.instance_token)
    -        _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -        async def handler(context, arguments):
    -            _ = context, arguments
    -            return root_name
    -
    -        async def apply(ctx) -> None:
    -            await ctx.require(TOOL_CATALOG).register(ctx, _definition(), handler)
    -
    -        fiber = await root.mount(
    -            apply,
    -            name="github-watch",
    -            inject=(TOOL_CATALOG,),
    -            runtime=_runtime(tmp_path, "github-watch"),
    -        )
    -        catalog = _freeze_plugin_tools(
    -            tools,
    -            root.instance_token,
    -            {"github-watch": f"{root_name}:generation"},
    -        )
    -        return root, fiber, catalog
    -
    -    candidate_root, candidate_fiber, candidate = await build("candidate")
    -    formal_root, formal_fiber, formal = await build("formal")
    -
    -    assert candidate.identity == formal.identity
    -    assert candidate.root_instance_token is candidate_root.instance_token
    -    assert formal.root_instance_token is formal_root.instance_token
    -    assert candidate.root_instance_token is not formal.root_instance_token
    -    assert candidate["inspect_repository"].is_live()
    -    assert formal["inspect_repository"].is_live()
    -    candidate_handler = candidate["inspect_repository"].handler
    -    formal_handler = formal["inspect_repository"].handler
    -    assert candidate_handler is not None and formal_handler is not None
    -    assert candidate_handler is not formal_handler
    -    assert await candidate_handler(object(), {}) == "candidate"
    -    assert await formal_handler(object(), {}) == "formal"
    -
    -    await candidate_fiber.dispose()
    -    assert not candidate["inspect_repository"].is_live()
    -    assert formal["inspect_repository"].is_live()
    -    await formal_fiber.dispose()
    -    await candidate_root.dispose()
    -    await formal_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_frozen_tool_catalog_rejects_generation_map_drift(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("tools:generation")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(TOOL_CATALOG).register(ctx, _definition())
    -
    -    _ = await root.mount(
    -        apply,
    -        name="github-watch",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "github-watch"),
    -    )
    -    _ = _freeze_plugin_tools(
    -        tools,
    -        root.instance_token,
    -        {"github-watch": "generation:one"},
    -    )
    -    with pytest.raises(RuntimeError, match="generation identity 已冻结"):
    -        _freeze_plugin_tools(
    -            tools,
    -            root.instance_token,
    -            {"github-watch": "generation:two"},
    -        )
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_tool_catalog_rejects_duplicate_name_without_partial_registration(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("tools:test")
    -    tools = PluginTools(root.instance_token)
    -    _ = await root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def register(ctx) -> None:
    -        await ctx.require(TOOL_CATALOG).register(ctx, _definition())
    -
    -    first = await root.mount(
    -        register,
    -        name="first",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "first"),
    -    )
    -    second = await root.mount(
    -        register,
    -        name="second",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "second"),
    -    )
    -    assert second.state.value == "failed"
    -    assert isinstance(second.error, CompositionError)
    -    assert second.error.code == "DUPLICATE_PLUGIN_TOOL"
    -
    -    catalog = _freeze_plugin_tools(
    -        tools,
    -        root.instance_token,
    -        {"first": "first:generation"},
    -    )
    -    assert tuple(catalog) == ("inspect_repository",)
    -    assert catalog["inspect_repository"].plugin_id == "first"
    -    await first.dispose()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_tool_facade_rejects_cross_root_registration(
    -    tmp_path: Path,
    -) -> None:
    -    owner_root = CompositionRoot("owner:test")
    -    foreign_root = CompositionRoot("foreign:test")
    -    tools = PluginTools(owner_root.instance_token)
    -    _ = await foreign_root.context.provide(TOOL_CATALOG, tools)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(TOOL_CATALOG).register(ctx, _definition())
    -
    -    fiber = await foreign_root.mount(
    -        apply,
    -        name="foreign",
    -        inject=(TOOL_CATALOG,),
    -        runtime=_runtime(tmp_path, "foreign"),
    -    )
    -    assert fiber.state.value == "failed"
    -    assert isinstance(fiber.error, CompositionError)
    -    assert fiber.error.code == "PLUGIN_TOOLS_SERVICE_ROOT_MISMATCH"
    -
    -    await owner_root.dispose()
    -    await foreign_root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_compiles_and_executes_exact_v3_tool_binding(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = tmp_path / "plugins" / "github-watch"
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import TOOL_CATALOG, PluginToolDefinition\n"
    -        "api_version = 3\n"
    -        "name = 'github-watch'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (TOOL_CATALOG,)\n"
    -        "bound_data_dir = None\n"
    -        "async def inspect_repository(context, arguments):\n"
    -        "    return context.turn_id + ':' + str(arguments['repository']) + ':' + str(bound_data_dir)\n"
    -        "async def mount_tools(ctx):\n"
    -        "    await ctx.require(TOOL_CATALOG).register(ctx, PluginToolDefinition(\n"
    -        "        name='inspect_repository',\n"
    -        "        description='Inspect one repository.',\n"
    -        "        parameters={\n"
    -        "            'type': 'object',\n"
    -        "            'properties': {'repository': {'type': 'string'}},\n"
    -        "            'required': ['repository'],\n"
    -        "            'additionalProperties': False,\n"
    -        "        },\n"
    -        "        handler_export='inspect_repository',\n"
    -        "        risk='read-only',\n"
    -        "    ))\n"
    -        "async def apply(ctx, config):\n"
    -        "    global bound_data_dir\n"
    -        "    bound_data_dir = ctx.data_root\n"
    -        "    await ctx.mount(mount_tools, inject=(TOOL_CATALOG,))\n",
    -        encoding="utf-8",
    -    )
    -    registry = ToolRegistry(validate_semantic_schema=False)
    -    manager = PluginManager(
    -        plugin_dirs=[plugin_dir.parent],
    -        event_bus=EventBus(),
    -        tool_registry=registry,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert snapshot.plugin_tool_catalog is not None
    -    assert snapshot.composition_root is not None
    -    assert (
    -        snapshot.plugin_tool_catalog.root_instance_token
    -        is snapshot.composition_root.instance_token
    -    )
    -    assert snapshot.tool_registry is not None
    -    lease = manager.snapshot_store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    snapshot.tool_registry.set_context(turn_id="turn:test")
    -    try:
    -        result = await snapshot.tool_registry.execute(
    -            "inspect_repository",
    -            {"repository": "akashic-agent"},
    -            raise_errors=True,
    -        )
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -    assert isinstance(result, str)
    -    assert result.startswith("turn:test:akashic-agent:")
    -    assert "plugin-validation" not in result
    -
    -    stable_binding = snapshot.plugin_tool_catalog["inspect_repository"]
    -    source = (plugin_dir / "plugin.py").read_text(encoding="utf-8")
    -    (plugin_dir / "plugin.py").write_text(
    -        source.replace("version = '1.0.0'", "version = '2.0.0'"),
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("github-watch")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_catalog = candidate.runtime_snapshot.plugin_tool_catalog
    -    assert candidate_catalog is not None
    -    candidate_binding = candidate_catalog["inspect_repository"]
    -    assert candidate_catalog is not snapshot.plugin_tool_catalog
    -    assert candidate_catalog.identity == snapshot.plugin_tool_catalog.identity
    -
    -    transaction = manager.snapshot_store.begin_publish(candidate.runtime_snapshot)
    -    await manager.snapshot_store.commit_latest(transaction)
    -    candidate_lease = manager.snapshot_store.lease(selector="latest")
    -    candidate_token = bind_runtime_snapshot(candidate_lease)
    -    candidate.runtime_snapshot.tool_registry.set_context(turn_id="turn:candidate")
    -    try:
    -        candidate_result = await candidate.runtime_snapshot.tool_registry.execute(
    -            "inspect_repository",
    -            {"repository": "akashic-agent"},
    -            raise_errors=True,
    -        )
    -    finally:
    -        reset_runtime_snapshot(candidate_token)
    -        await candidate_lease.release()
    -    assert isinstance(candidate_result, str)
    -    assert "turn:candidate:akashic-agent:" in candidate_result
    -    assert "plugin-validation" in candidate_result
    -
    -    await manager.snapshot_store.discard_latest(candidate.runtime_snapshot)
    -    assert not candidate_binding.is_live()
    -    await manager.discard_prepared("github-watch")
    -    await manager.terminate_all()
    -    assert not stable_binding.is_live()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("candidate_tool", "expected_tool"),
    -    (("recall_v2", "recall_v2"), (None, None)),
    -)
    -async def test_overlay_publish_switches_stable_background_consumer_catalog(
    -    tmp_path: Path,
    -    candidate_tool: str | None,
    -    expected_tool: str | None,
    -) -> None:
    -    plugin_dir = tmp_path / "plugins"
    -    provider_dir = plugin_dir / "memory-provider"
    -    consumer_dir = plugin_dir / "wake-consumer"
    -    provider_dir.mkdir(parents=True)
    -    consumer_dir.mkdir(parents=True)
    -
    -    def provider_source(version: str, tool_name: str | None) -> str:
    -        source = "".join(
    -            (
    -                "from agent.plugin_composition import TOOL_CATALOG, "
    -                "PluginToolDefinition, ServiceKey\n"
    -                "MEMORY_RECALL = ServiceKey[object]('memory.recall.v1')\n"
    -                "api_version = 3\n"
    -                "name = 'memory-provider'\n"
    -                f"version = '{version}'\n",
    -                "inject = (TOOL_CATALOG,)\n" if tool_name else "inject = ()\n",
    -                "async def recall(context, arguments): return 'ok'\n"
    -                "async def apply(ctx, config):\n"
    -                "    await ctx.provide(MEMORY_RECALL, object())\n",
    -            )
    -        )
    -        if tool_name is None:
    -            return source
    -        return source + (
    -            "    await ctx.require(TOOL_CATALOG).register(\n"
    -            "        ctx, PluginToolDefinition(\n"
    -            f"            name='{tool_name}', description='Recall memory.',\n"
    -            "            parameters={'type':'object','properties':{},'required':[],"
    -            "'additionalProperties':False},\n"
    -            "            handler_export='recall', risk='read-only'),\n"
    -            "        recall, provided_for=MEMORY_RECALL)\n"
    -        )
    -
    -    (provider_dir / "plugin.py").write_text(
    -        provider_source("1.0.0", "recall_v1"),
    -        encoding="utf-8",
    -    )
    -    (consumer_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import TOOL_CATALOG, ServiceKey\n"
    -        "MEMORY_RECALL = ServiceKey[object]('memory.recall.v1')\n"
    -        "api_version = 3\n"
    -        "name = 'wake-consumer'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (TOOL_CATALOG, MEMORY_RECALL)\n"
    -        "tools = None\n"
    -        "async def apply(ctx, config):\n"
    -        "    global tools\n"
    -        "    ctx.require(MEMORY_RECALL)\n"
    -        "    tools = ctx.require(TOOL_CATALOG)\n"
    -        "def resolved(): return tools.from_provide(MEMORY_RECALL)\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[plugin_dir],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -    consumer = stable.generations["wake-consumer"].instance.module
    -    assert consumer.resolved() == "recall_v1"
    -
    -    (provider_dir / "plugin.py").write_text(
    -        provider_source("2.0.0", candidate_tool),
    -        encoding="utf-8",
    -    )
    -    candidate = await manager.prepare_candidate("memory-provider")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_catalog = candidate.runtime_snapshot.plugin_tool_catalog
    -    assert candidate_catalog is not None
    -    assert set(candidate_catalog) == ({candidate_tool} if candidate_tool else set())
    -    assert consumer.tools in candidate.runtime_snapshot.plugin_tool_facades
    -    transaction = manager.snapshot_store.begin_publish(candidate.runtime_snapshot)
    -    await manager.snapshot_store.commit_latest(transaction)
    -    assert consumer.resolved() == "recall_v1"
    -
    -    candidate.runtime_snapshot.accepting_leases = False
    -    manager.snapshot_store.seal_candidate_validation(candidate.runtime_snapshot)
    -    with pytest.raises(RuntimeError, match="rollback fixture"):
    -        await manager.snapshot_store.promote_latest(
    -            after_open=lambda: (_ for _ in ()).throw(RuntimeError("rollback fixture"))
    -        )
    -    assert consumer.resolved() == "recall_v1"
    -
    -    candidate.runtime_snapshot.accepting_leases = False
    -    _ = await manager.snapshot_store.promote_latest()
    -    if expected_tool is None:
    -        with pytest.raises(CompositionError) as raised:
    -            _ = consumer.resolved()
    -        assert raised.value.code == "PROVIDED_TOOL_NOT_BOUND"
    -    else:
    -        assert consumer.resolved() == expected_tool
    -
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_rejects_malformed_tool_handler_before_publication(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = tmp_path / "plugins" / "broken-tool"
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import TOOL_CATALOG, PluginToolDefinition\n"
    -        "api_version = 3\n"
    -        "name = 'broken-tool'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (TOOL_CATALOG,)\n"
    -        "async def broken(arguments):\n"
    -        "    return 'bad'\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(TOOL_CATALOG).register(ctx, PluginToolDefinition(\n"
    -        "        name='broken_tool', description='Broken Tool.',\n"
    -        "        parameters={'type': 'object', 'properties': {}, 'required': [], "
    -        "'additionalProperties': False},\n"
    -        "        handler_export='broken',\n"
    -        "    ))\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[plugin_dir.parent],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -    assert manager.current_snapshot is None
    -    assert manager.snapshot_store.current is None
    -    assert manager.generation("broken-tool") is None
    -    assert not (tmp_path / "workspace" / "plugin-data" / "broken-tool-builtin").exists()
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_incremental_load_rolls_back_new_tool_data_root_on_admission_failure(
    -    tmp_path: Path,
    -) -> None:
    -    plugins = tmp_path / "plugins"
    -    stable_dir = plugins / "stable-v3"
    -    stable_dir.mkdir(parents=True)
    -    (stable_dir / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'stable-v3'\n"
    -        "version = '1.0.0'\n"
    -        "async def apply(ctx, config):\n"
    -        "    return None\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[plugins],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -
    -    broken_dir = plugins / "broken-tool"
    -    broken_dir.mkdir()
    -    (broken_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import TOOL_CATALOG, PluginToolDefinition\n"
    -        "api_version = 3\n"
    -        "name = 'broken-tool'\n"
    -        "version = '1.0.0'\n"
    -        "inject = (TOOL_CATALOG,)\n"
    -        "async def broken(arguments):\n"
    -        "    return 'bad'\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(TOOL_CATALOG).register(ctx, PluginToolDefinition(\n"
    -        "        name='broken_tool', description='Broken Tool.',\n"
    -        "        parameters={'type': 'object', 'properties': {}, 'required': [], "
    -        "'additionalProperties': False}, handler_export='broken'))\n",
    -        encoding="utf-8",
    -    )
    -    await manager.load_all()
    -
    -    assert manager.current_snapshot is stable
    -    assert manager.generation("broken-tool") is None
    -    assert not (tmp_path / "workspace" / "plugin-data" / "broken-tool-builtin").exists()
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_composition_ui_slots.py b/tests/test_plugin_composition_ui_slots.py
    deleted file mode 100644
    index 0b621bebc..000000000
    --- a/tests/test_plugin_composition_ui_slots.py
    +++ /dev/null
    @@ -1,457 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from pathlib import Path
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    UI_SLOTS,
    -    CompositionError,
    -    CompositionRoot,
    -    MobileUiDefinition,
    -    MobileUiNavigation,
    -    PluginRuntime,
    -    PluginUiSlots,
    -    resolve_mobile_ui_asset,
    -)
    -from agent.plugin_composition.ui_slots import MobileUiSlot
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.mobile_ui import (
    -    MobileUiPluginUnavailable,
    -    PluginMobileUiProvider,
    -)
    -from bus.event_bus import EventBus
    -
    -
    -def _runtime(plugin_dir: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id=plugin_dir.name,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _write_plugin(root: Path, name: str, source: str) -> Path:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(source, encoding="utf-8")
    -    return plugin_dir
    -
    -
    -def _definition() -> MobileUiDefinition:
    -    return MobileUiDefinition(
    -        module="mobile.js",
    -        stylesheet="mobile.css",
    -        navigation=MobileUiNavigation(label="Probe", description="Probe panel"),
    -        slots=("drawer.panel",),
    -    )
    -
    -
    -def _query(
    -    method: str,
    -    payload: dict[str, object],
    -    *,
    -    session_id: str | None,
    -    turn_id: str | None,
    -) -> dict[str, object]:
    -    return {
    -        "method": method,
    -        "payload": payload,
    -        "session_id": session_id,
    -        "turn_id": turn_id,
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_ui_slots_freeze_descriptor_and_effect_cleanup(tmp_path: Path) -> None:
    -    plugin_dir = tmp_path / "probe"
    -    plugin_dir.mkdir()
    -    (plugin_dir / "mobile.js").write_text("export const probe = true;\n", encoding="utf-8")
    -    (plugin_dir / "mobile.css").write_text(":host {}\n", encoding="utf-8")
    -    root = CompositionRoot("ui-slots")
    -    slots = PluginUiSlots()
    -    _ = await root.context.provide(UI_SLOTS, slots)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(UI_SLOTS).register_mobile(
    -            ctx,
    -            _definition(),
    -            query=_query,
    -        )
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="probe",
    -        inject=(UI_SLOTS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    registry = slots.freeze()
    -    binding = registry["probe"]
    -    assert registry.descriptor("probe") is binding.descriptor
    -    assert binding.descriptor.owner == "probe"
    -    assert binding.descriptor.module_bytes == len(binding.asset.module.encode())
    -    assert registry.identity
    -
    -    await fiber.dispose()
    -    assert slots.freeze() is registry
    -    assert len(registry) == 1
    -    assert not registry["probe"].is_live()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_ui_slots_rejects_duplicate_and_frozen_registration(tmp_path: Path) -> None:
    -    plugin_dir = tmp_path / "probe"
    -    plugin_dir.mkdir()
    -    (plugin_dir / "mobile.js").write_text("export default 1;", encoding="utf-8")
    -    (plugin_dir / "mobile.css").write_text("", encoding="utf-8")
    -    root = CompositionRoot("ui-slots-duplicate")
    -    slots = PluginUiSlots()
    -    _ = await root.context.provide(UI_SLOTS, slots)
    -    captured: dict[str, Any] = {}
    -
    -    async def apply(ctx) -> None:
    -        captured["ctx"] = ctx
    -        service = ctx.require(UI_SLOTS)
    -        await service.register_mobile(ctx, _definition(), query=_query)
    -        await service.register_mobile(ctx, _definition(), query=_query)
    -
    -    _ = await root.mount(
    -        apply,
    -        name="probe",
    -        inject=(UI_SLOTS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    assert not root.receipt().ready
    -    assert any(
    -        "只能声明一个 Mobile UI" in (fiber.error or "")
    -        for fiber in root.receipt().fibers
    -    )
    -    assert len(slots.freeze()) == 0
    -    await root.dispose()
    -
    -    root = CompositionRoot("ui-slots-frozen")
    -    slots = PluginUiSlots()
    -    _ = await root.context.provide(UI_SLOTS, slots)
    -    ctx: Any = None
    -
    -    async def register_once(current) -> None:
    -        nonlocal ctx
    -        ctx = current
    -        await ctx.require(UI_SLOTS).register_mobile(ctx, _definition(), query=_query)
    -
    -    _ = await root.mount(
    -        register_once,
    -        name="probe",
    -        inject=(UI_SLOTS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    _ = slots.freeze()
    -    with pytest.raises(CompositionError, match="已冻结"):
    -        await slots.register_mobile(ctx, _definition(), query=_query)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_ui_slots_rejects_symlink_and_async_handler(tmp_path: Path) -> None:
    -    plugin_dir = tmp_path / "probe"
    -    plugin_dir.mkdir()
    -    outside = tmp_path / "outside.js"
    -    outside.write_text("export default 1;", encoding="utf-8")
    -    (plugin_dir / "mobile.js").symlink_to(outside)
    -    root = CompositionRoot("ui-slots-path")
    -    slots = PluginUiSlots()
    -    _ = await root.context.provide(UI_SLOTS, slots)
    -
    -    async def apply(ctx) -> None:
    -        async def bad(*args: object, **kwargs: object) -> object:
    -            return {}
    -
    -        await ctx.require(UI_SLOTS).register_mobile(
    -            ctx,
    -            _definition(),
    -            query=cast(Any, bad),
    -        )
    -
    -    _ = await root.mount(
    -        apply,
    -        name="probe",
    -        inject=(UI_SLOTS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    receipt = root.receipt()
    -    assert not receipt.ready
    -    assert any(
    -        "必须是同步函数" in (fiber.error or "") for fiber in receipt.fibers
    -    )
    -    await root.dispose()
    -
    -
    -@pytest.mark.parametrize(
    -    ("definition", "message"),
    -    (
    -        (
    -            MobileUiDefinition(
    -                module="mobile.js", slots=(cast(MobileUiSlot, "unknown.slot"),)
    -            ),
    -            "slots 无效",
    -        ),
    -        (
    -            MobileUiDefinition(
    -                module="mobile.js",
    -                navigation=MobileUiNavigation(label="", description="Probe"),
    -            ),
    -            "navigation 无效",
    -        ),
    -    ),
    -)
    -def test_mobile_ui_asset_rejects_invalid_metadata(
    -    tmp_path: Path,
    -    definition: MobileUiDefinition,
    -    message: str,
    -) -> None:
    -    plugin_dir = tmp_path / "probe"
    -    plugin_dir.mkdir()
    -    (plugin_dir / "mobile.js").write_text("export default 1;", encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match=message):
    -        resolve_mobile_ui_asset(
    -            plugin_dir,
    -            module=definition.module,
    -            stylesheet=definition.stylesheet,
    -            navigation_label=(
    -                None
    -                if definition.navigation is None
    -                else definition.navigation.label
    -            ),
    -            navigation_description=(
    -                None
    -                if definition.navigation is None
    -                else definition.navigation.description
    -            ),
    -            slots=tuple(definition.slots),
    -        )
    -
    -
    -def test_mobile_ui_asset_rejects_size_over_budget(tmp_path: Path) -> None:
    -    plugin_dir = tmp_path / "probe"
    -    plugin_dir.mkdir()
    -    (plugin_dir / "mobile.js").write_text("x" * (240 * 1024 + 1), encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="超过协议安全预算"):
    -        resolve_mobile_ui_asset(
    -            plugin_dir,
    -            module="mobile.js",
    -            stylesheet=None,
    -            navigation_label=None,
    -            navigation_description=None,
    -            slots=(),
    -        )
    -
    -
    -def _manager(tmp_path: Path) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -
    -
    -def _plugin_source(version: str) -> str:
    -    return (
    -        "from agent.plugin_composition import UI_SLOTS, MobileUiDefinition\n"
    -        "api_version = 3\n"
    -        "name = 'ui_probe'\n"
    -        f"version = '{version}'\n"
    -        "inject = (UI_SLOTS,)\n"
    -        "def query(method, payload, *, session_id, turn_id):\n"
    -        "    return {'version': version, 'method': method, 'payload': payload}\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(UI_SLOTS).register_mobile(\n"
    -        "        ctx, MobileUiDefinition(module='mobile.js', slots=('drawer.panel',)),\n"
    -        "        query=query,\n"
    -        "    )\n"
    -    )
    -
    -
    -def _nested_plugin_source() -> str:
    -    return (
    -        "from agent.plugin_composition import UI_SLOTS, MobileUiDefinition\n"
    -        "api_version = 3\n"
    -        "name = 'ui_probe'\n"
    -        "version = '1'\n"
    -        "inject = (UI_SLOTS,)\n"
    -        "child_handle = None\n"
    -        "def query(method, payload, *, session_id, turn_id):\n"
    -        "    return {'status': 'ready', 'method': method}\n"
    -        "async def register_mobile(ctx):\n"
    -        "    await ctx.require(UI_SLOTS).register_mobile(\n"
    -        "        ctx, MobileUiDefinition(module='mobile.js', slots=('drawer.panel',)),\n"
    -        "        query=query,\n"
    -        "    )\n"
    -        "async def apply(ctx, config):\n"
    -        "    global child_handle\n"
    -        "    child_handle = await ctx.mount(\n"
    -        "        register_mobile, name='mobile-nested', inject=(UI_SLOTS,),\n"
    -        "    )\n"
    -    )
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("transition", ("dispose", "restart"))
    -async def test_published_snapshot_hides_nested_fiber_after_transition(
    -    tmp_path: Path,
    -    transition: str,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "ui_probe", _nested_plugin_source())
    -    (plugin_dir / "mobile.js").write_text("export const nested = true;\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -
    -    generation = manager.generation("ui_probe")
    -    snapshot = manager.current_snapshot
    -    assert generation is not None and snapshot is not None
    -    root = snapshot.composition_root
    -    assert root is not None
    -    child = cast(Any, generation.instance.module).child_handle
    -    assert child is not None
    -    provider = PluginMobileUiProvider(manager)
    -    assert provider.catalog()["items"]
    -
    -    if transition == "dispose":
    -        await child.dispose()
    -    else:
    -        await child.restart()
    -        assert any(
    -            "已冻结" in (fiber.error or "") for fiber in root.receipt().fibers
    -        )
    -
    -    assert provider.catalog()["items"] == []
    -    with pytest.raises(MobileUiPluginUnavailable):
    -        await provider.query(
    -            "ui_probe",
    -            generation.source_revision,
    -            "probe.current",
    -            {},
    -            session_id=None,
    -            turn_id=None,
    -        )
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_publishes_v3_registry_without_generation_contribution(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "ui_probe", _plugin_source("1"))
    -    (plugin_dir / "mobile.js").write_text("export const version = 1;\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -
    -    generation = manager.generation("ui_probe")
    -    snapshot = manager.current_snapshot
    -    assert generation is not None and snapshot is not None
    -    assert snapshot.mobile_ui_registry is not None
    -    provider = PluginMobileUiProvider(manager)
    -    item = cast(list[dict[str, object]], provider.catalog()["items"])[0]
    -    assert item["id"] == "ui_probe"
    -    result = await provider.query(
    -        "ui_probe",
    -        generation.source_revision,
    -        "probe.current",
    -        {"limit": 1},
    -        session_id="mobile:test",
    -        turn_id="turn-1",
    -    )
    -    assert result == {
    -        "version": "1",
    -        "method": "probe.current",
    -        "payload": {"limit": 1},
    -    }
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_candidate_registry_stays_private_until_publish(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "ui_probe", _plugin_source("1"))
    -    (plugin_dir / "mobile.js").write_text("export const version = 1;\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    stable = manager.current_snapshot
    -    assert stable is not None
    -    stable_generation = manager.generation("ui_probe")
    -    assert stable_generation is not None
    -
    -    (plugin_dir / "plugin.py").write_text(_plugin_source("2"), encoding="utf-8")
    -    (plugin_dir / "mobile.js").write_text("export const version = 2;\n", encoding="utf-8")
    -    candidate = await manager.prepare_candidate("ui_probe")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    assert manager.current_snapshot is stable
    -    assert manager.current_snapshot.mobile_ui_registry is not None
    -    assert (
    -        candidate.runtime_snapshot.mobile_ui_registry is not None
    -        and candidate.runtime_snapshot.mobile_ui_registry["ui_probe"].asset.module
    -        == "export const version = 2;\n"
    -    )
    -    provider = PluginMobileUiProvider(manager)
    -    stable_item = cast(list[dict[str, object]], provider.catalog()["items"])[0]
    -    assert stable_item["module_bytes"] == len("export const version = 1;\n".encode())
    -    stable_result = await provider.query(
    -        "ui_probe",
    -        stable_generation.source_revision,
    -        "probe.current",
    -        {},
    -        session_id=None,
    -        turn_id=None,
    -    )
    -    assert stable_result["version"] == "1"
    -    await manager.discard_prepared("ui_probe")
    -    assert manager.current_snapshot is stable
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_publish_rebuilds_formal_ui_handler(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_dir = _write_plugin(tmp_path / "plugins", "ui_probe", _plugin_source("1"))
    -    (plugin_dir / "mobile.js").write_text("export const version = 1;\n", encoding="utf-8")
    -    manager = _manager(tmp_path)
    -    await manager.load_all()
    -    (plugin_dir / "plugin.py").write_text(_plugin_source("2"), encoding="utf-8")
    -    (plugin_dir / "mobile.js").write_text("export const version = 2;\n", encoding="utf-8")
    -
    -    candidate = await manager.prepare_candidate("ui_probe")
    -    assert candidate is not None and candidate.runtime_snapshot is not None
    -    candidate_registry = candidate.runtime_snapshot.mobile_ui_registry
    -    assert candidate_registry is not None
    -
    -    result = await manager.publish_prepared("ui_probe")
    -    assert result["publication_state"] == "committed"
    -    snapshot = manager.current_snapshot
    -    generation = manager.generation("ui_probe")
    -    assert snapshot is not None and generation is not None
    -    registry = snapshot.mobile_ui_registry
    -    assert registry is not None
    -    assert registry["ui_probe"].asset.module == "export const version = 2;\n"
    -    assert registry["ui_probe"].query is not candidate_registry["ui_probe"].query
    -    provider = PluginMobileUiProvider(manager)
    -    response = await provider.query(
    -        "ui_probe",
    -        generation.source_revision,
    -        "probe.current",
    -        {},
    -        session_id=None,
    -        turn_id=None,
    -    )
    -    assert response["version"] == "2"
    -    await manager.terminate_all()
    diff --git a/tests/test_plugin_config_schema.py b/tests/test_plugin_config_schema.py
    deleted file mode 100644
    index 5004e9ade..000000000
    --- a/tests/test_plugin_config_schema.py
    +++ /dev/null
    @@ -1,113 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.config import Config
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -
    -
    -def _write_typed_plugin(root: Path) -> None:
    -    plugin_dir = root / "typed"
    -    plugin_dir.mkdir()
    -    (plugin_dir / "plugin.py").write_text(
    -        """
    -from pydantic import BaseModel
    -
    -api_version = 3
    -name = "typed"
    -version = "1.0.0"
    -
    -
    -class TypedConfig(BaseModel):
    -    api_key: str
    -    max_results: int = 5
    -
    -
    -Config = TypedConfig
    -
    -
    -async def apply(ctx, config):
    -    return None
    -""".strip(),
    -        encoding="utf-8",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_config_model_validates_and_injects_config(tmp_path: Path):
    -    _write_typed_plugin(tmp_path)
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    data_dir = workspace / "plugin-data" / "typed-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        'api_key = "secret"\nmax_results = 9\n',
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=plugins_home / "cache",
    -    )
    -
    -    await manager.load_all()
    -
    -    generation = manager.generation("typed")
    -    assert generation is not None
    -    config = generation.config
    -    assert isinstance(config, generation.instance.module.Config)
    -    assert config.api_key == "secret"
    -    assert config.max_results == 9
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_config_model_failure_skips_plugin(tmp_path: Path):
    -    _write_typed_plugin(tmp_path)
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    data_dir = workspace / "plugin-data" / "typed-builtin"
    -    data_dir.mkdir(parents=True)
    -    (data_dir / "config.local.toml").write_text(
    -        'max_results = "bad"\n', encoding="utf-8"
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=plugins_home / "cache",
    -    )
    -
    -    await manager.load_all()
    -
    -    assert manager.loaded_count == 0
    -    await manager.terminate_all()
    -
    -
    -def test_config_load_ignores_plugin_owned_sections(tmp_path: Path, monkeypatch):
    -    config_path = tmp_path / "config.toml"
    -    config_path.write_text(
    -        """
    -[agent]
    -system_prompt = "s"
    -
    -[plugins.typed]
    -api_key = "${PLUGIN_TOKEN}"
    -
    -[channels.qqbot]
    -app_id = "app"
    -client_secret = "${QQBOT_SECRET}"
    -allow_from = ["user-openid"]
    -""".strip() + "\n",
    -        encoding="utf-8",
    -    )
    -    monkeypatch.setenv("PLUGIN_TOKEN", "plugin-secret")
    -    monkeypatch.setenv("QQBOT_SECRET", "qq-secret")
    -
    -    config = Config.load(config_path, workspace=tmp_path)
    -
    -    assert not hasattr(config, "plugins")
    diff --git a/tests/test_plugin_doctor.py b/tests/test_plugin_doctor.py
    deleted file mode 100644
    index c16c61d0f..000000000
    --- a/tests/test_plugin_doctor.py
    +++ /dev/null
    @@ -1,522 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -
    -import pytest
    -
    -import agent.plugins.doctor as plugin_doctor
    -from agent.plugins.artifacts import ArtifactPointer, write_pointers
    -from agent.plugins.doctor import format_plugin_doctor_report, run_plugin_doctor
    -from agent.plugins.manifest import upsert_plugin_manifest
    -def _write_artifact_plugin(
    -    plugin_base: Path,
    -    artifact_id: str,
    -    *,
    -    skills: dict[str, str],
    -) -> Path:
    -    plugin_root = plugin_base / ".artifacts" / artifact_id
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'demo'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('skills',)\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_manifest(plugin_root, name="demo")
    -    for name, body in skills.items():
    -        skill_dir = plugin_root / "skills" / name
    -        skill_dir.mkdir(parents=True)
    -        (skill_dir / "SKILL.md").write_text(body, encoding="utf-8")
    -    return plugin_root
    -
    -
    -def _write_static_manifest(
    -    plugin_root: Path,
    -    *,
    -    name: str,
    -    entrypoint: str = "plugin.py",
    -    mcp_names: tuple[str, ...] = (),
    -) -> None:
    -    mcp_manifest = ""
    -    for mcp_name in mcp_names:
    -        runner = plugin_root / "mcp" / f"{mcp_name}.py"
    -        runner.parent.mkdir(parents=True, exist_ok=True)
    -        runner.write_text("", encoding="utf-8")
    -        mcp_manifest += (
    -            "\n[[mcp]]\n" f"name = {mcp_name!r}\n" f"command = ['mcp/{mcp_name}.py']\n"
    -        )
    -    (plugin_root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        f"name = {name!r}\n"
    -        "version = '1.0.0'\n"
    -        "api_version = 3\n"
    -        f"entrypoint = {entrypoint!r}\n"
    -        f"{mcp_manifest}",
    -        encoding="utf-8",
    -    )
    -
    -
    -def _write_builtin_plugin(
    -    builtin_root: Path,
    -    folder: str,
    -    declared_name: str,
    -    *,
    -    static: bool = True,
    -) -> Path:
    -    plugin_root = builtin_root / folder
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        f"name = {declared_name!r}\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    if static:
    -        _write_static_manifest(plugin_root, name=declared_name)
    -    return plugin_root
    -
    -
    -def _check(report: dict[str, object], name: str) -> dict[str, str]:
    -    plugins = report["plugins"]
    -    assert isinstance(plugins, list)
    -    checks = plugins[0]["checks"]
    -    assert isinstance(checks, list)
    -    return next(check for check in checks if check["name"] == name)
    -
    -
    -def test_plugin_doctor_reads_programmatic_capabilities(tmp_path: Path) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "github" / "demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    skill_dir = plugin_root / "skills" / "demo-skill"
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text("skill", encoding="utf-8")
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'demo'\n"
    -        "version = '1.0.0'\n"
    -        "skill_roots = ('skills',)\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_manifest(plugin_root, name="demo")
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    (workspace / "skills").mkdir(parents=True)
    -    (workspace / "skills" / "demo-skill").symlink_to(
    -        skill_dir, target_is_directory=True
    -    )
    -    upsert_plugin_manifest("demo@github", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@github",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert "plugin doctor demo@github" in format_plugin_doctor_report(report)
    -
    -
    -def test_plugin_doctor_reads_v3_namespace_declaration(tmp_path: Path) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "github" / "v3_demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'v3_demo'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_manifest(
    -        plugin_root,
    -        name="v3_demo",
    -        mcp_names=("fitbit", "steam"),
    -    )
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    upsert_plugin_manifest("v3_demo@github", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="v3_demo@github",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert _check(report, "skills")["detail"].startswith("roots=0")
    -    assert _check(report, "mcp")["detail"] == (
    -        "declared_servers=2 names=['fitbit', 'steam']"
    -    )
    -
    -
    -def test_plugin_doctor_uses_static_custom_entrypoint(tmp_path: Path) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "github" / "static_demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "entry.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'static_demo'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'static_demo'\n"
    -        "version = '1.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'entry.py'\n",
    -        encoding="utf-8",
    -    )
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    upsert_plugin_manifest(
    -        "static_demo@github",
    -        enabled=True,
    -        plugins_home=plugins_home,
    -    )
    -
    -    report = run_plugin_doctor(
    -        plugin_id="static_demo@github",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert "entry.py" in format_plugin_doctor_report(report)
    -
    -
    -def test_plugin_doctor_custom_entrypoint_uses_its_relative_import_root(
    -    tmp_path: Path,
    -) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "github" / "nested_demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    (plugin_root / "src").mkdir(parents=True)
    -    (plugin_root / "src" / "constants.py").write_text(
    -        "VERSION = '1.0.0'\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_root / "src" / "entry.py").write_text(
    -        "from .constants import VERSION\n"
    -        "api_version = 3\n"
    -        "name = 'nested_demo'\n"
    -        "version = VERSION\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'nested_demo'\n"
    -        "version = '1.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'src/entry.py'\n",
    -        encoding="utf-8",
    -    )
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    upsert_plugin_manifest(
    -        "nested_demo@github",
    -        enabled=True,
    -        plugins_home=plugins_home,
    -    )
    -
    -    report = run_plugin_doctor(
    -        plugin_id="nested_demo@github",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert "src/entry.py" in format_plugin_doctor_report(report)
    -
    -
    -def test_plugin_doctor_reads_latest_artifact_candidate(tmp_path: Path) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "local" / "demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\n"
    -        "name = 'demo'\n"
    -        "version = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_manifest(plugin_root, name="demo")
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(None),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    upsert_plugin_manifest("demo@local", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@local",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "degraded"
    -    assert str(plugin_root) in format_plugin_doctor_report(report)
    -    assert _check(report, "candidate")["status"] == "deferred"
    -
    -
    -def test_plugin_doctor_rejects_legacy_visible_version_without_pointer(
    -    tmp_path: Path,
    -) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    plugin_root = plugins_home / "cache/github/demo/1.0.0"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text(
    -        "api_version = 3\nname = 'demo'\nversion = '1.0.0'\n"
    -        "def apply(ctx, config): pass\n",
    -        encoding="utf-8",
    -    )
    -    _write_static_manifest(plugin_root, name="demo")
    -    upsert_plugin_manifest("demo@github", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@github",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "broken"
    -    assert _check(report, "install")["detail"] == "未找到插件目录"
    -
    -
    -def test_plugin_doctor_defers_candidate_projection_until_promotion(
    -    tmp_path: Path,
    -) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "local" / "demo"
    -    stable_root = _write_artifact_plugin(
    -        plugin_base,
    -        "1.0.0-aaaa",
    -        skills={"stable-skill": "stable\n"},
    -    )
    -    latest_root = _write_artifact_plugin(
    -        plugin_base,
    -        "2.0.0-bbbb",
    -        skills={"candidate-skill": "candidate\n"},
    -    )
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/2.0.0-bbbb"),
    -    )
    -    link = workspace / "skills" / "stable-skill"
    -    link.parent.mkdir(parents=True)
    -    link.symlink_to(stable_root / "skills" / "stable-skill", target_is_directory=True)
    -    upsert_plugin_manifest("demo@local", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@local",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    assert report["status"] == "degraded"
    -    assert _check(report, "skills")["status"] == "ok"
    -    assert _check(report, "candidate")["status"] == "deferred"
    -    assert str(latest_root) in _check(report, "candidate")["detail"]
    -
    -
    -def test_plugin_doctor_reports_misdirected_and_stale_stable_projection(
    -    tmp_path: Path,
    -) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    workspace = tmp_path / "workspace"
    -    plugin_base = plugins_home / "cache" / "local" / "demo"
    -    old_root = _write_artifact_plugin(
    -        plugin_base,
    -        "1.0.0-aaaa",
    -        skills={"current-skill": "old\n", "removed-skill": "removed\n"},
    -    )
    -    stable_root = _write_artifact_plugin(
    -        plugin_base,
    -        "2.0.0-bbbb",
    -        skills={"current-skill": "current\n"},
    -    )
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/2.0.0-bbbb"),
    -        latest=ArtifactPointer(".artifacts/2.0.0-bbbb"),
    -    )
    -    skills_dir = workspace / "skills"
    -    skills_dir.mkdir(parents=True)
    -    (skills_dir / "current-skill").symlink_to(
    -        old_root / "skills" / "current-skill",
    -        target_is_directory=True,
    -    )
    -    (skills_dir / "removed-skill").symlink_to(
    -        old_root / "skills" / "removed-skill",
    -        target_is_directory=True,
    -    )
    -    upsert_plugin_manifest("demo@local", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@local",
    -        plugins_home=plugins_home,
    -        workspace=workspace,
    -    )
    -
    -    skills = _check(report, "skills")
    -    assert report["status"] == "degraded"
    -    assert skills["status"] == "warn"
    -    assert "misdirected=['current-skill']" in skills["detail"]
    -    assert "stale=['removed-skill']" in skills["detail"]
    -    assert stable_root != old_root
    -
    -
    -def test_plugin_doctor_reports_broken_declaration(tmp_path: Path) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    plugin_base = plugins_home / "cache" / "github" / "demo"
    -    plugin_root = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "plugin.py").write_text("class X: pass\n", encoding="utf-8")
    -    _write_static_manifest(plugin_root, name="demo")
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -    upsert_plugin_manifest("demo@github", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="demo@github",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "broken"
    -
    -
    -@pytest.mark.parametrize("plugin_id", ["wake", "openai-compatible", "opencode-go"])
    -def test_plugin_doctor_finds_builtin_plugin(
    -    tmp_path: Path,
    -    plugin_id: str,
    -) -> None:
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    upsert_plugin_manifest(plugin_id, enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id=plugin_id,
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "healthy"
    -
    -
    -def test_builtin_doctor_uses_one_declared_identity(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    fake_doctor = tmp_path / "repo" / "agent" / "plugins" / "doctor.py"
    -    builtin_root = tmp_path / "repo" / "plugins"
    -    monkeypatch.setattr(plugin_doctor, "__file__", str(fake_doctor))
    -    for folder, declared_name in (("wake", "other"), ("custom", "wake")):
    -        _write_builtin_plugin(builtin_root, folder, declared_name)
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    upsert_plugin_manifest("wake", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="wake",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert str(builtin_root / "custom") in _check(report, "install")["detail"]
    -
    -    duplicate = builtin_root / "duplicate"
    -    duplicate.mkdir()
    -    (duplicate / "plugin.py").write_text("", encoding="utf-8")
    -    _write_static_manifest(duplicate, name="wake")
    -    duplicate_report = run_plugin_doctor(
    -        plugin_id="wake",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert duplicate_report["status"] == "broken"
    -    assert (
    -        "多个内置插件声明了相同 name" in _check(duplicate_report, "install")["detail"]
    -    )
    -
    -
    -def test_builtin_doctor_reports_any_invalid_manifest(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    fake_doctor = tmp_path / "repo" / "agent" / "plugins" / "doctor.py"
    -    plugin_root = tmp_path / "repo" / "plugins" / "custom"
    -    plugin_root.mkdir(parents=True)
    -    (plugin_root / "akashic.plugin.toml").symlink_to("missing.toml")
    -    monkeypatch.setattr(plugin_doctor, "__file__", str(fake_doctor))
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    upsert_plugin_manifest("wake", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="wake",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "broken"
    -    assert "静态 manifest" in _check(report, "install")["detail"]
    -
    -
    -def test_builtin_doctor_ignores_symlink_plugin_root(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    fake_doctor = tmp_path / "repo" / "agent" / "plugins" / "doctor.py"
    -    builtin_root = tmp_path / "repo" / "plugins"
    -    monkeypatch.setattr(plugin_doctor, "__file__", str(fake_doctor))
    -    legacy_root = _write_builtin_plugin(
    -        builtin_root,
    -        "wake",
    -        "wake",
    -        static=False,
    -    )
    -    symlink_target = _write_builtin_plugin(tmp_path, "outside", "shadow")
    -    (builtin_root / "custom").symlink_to(symlink_target, target_is_directory=True)
    -    plugins_home = tmp_path / ".akashic-plugin"
    -    upsert_plugin_manifest("wake", enabled=True, plugins_home=plugins_home)
    -
    -    report = run_plugin_doctor(
    -        plugin_id="wake",
    -        plugins_home=plugins_home,
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    assert report["status"] == "healthy"
    -    assert str(legacy_root) in _check(report, "install")["detail"]
    diff --git a/tests/test_plugin_generation_activity_host.py b/tests/test_plugin_generation_activity_host.py
    deleted file mode 100644
    index 17b15cd95..000000000
    --- a/tests/test_plugin_generation_activity_host.py
    +++ /dev/null
    @@ -1,507 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -from types import SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugins.generation_activity_host import (
    -    ActivityCatalog,
    -    ActivityHost,
    -)
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler, RuntimeSnapshotStore
    -
    -
    -class _RecordingChild:
    -    name = "recording"
    -
    -    def __init__(self) -> None:
    -        self.events: list[str] = []
    -        self.materialized = 0
    -
    -    def prepare_components(self, transaction_id, target_lease, target_catalog):
    -        assert target_lease.active
    -        assert isinstance(target_catalog, ActivityCatalog)
    -        self.events.append("prepare")
    -        return target_lease.snapshot.snapshot_id
    -
    -    def discard_plan(self, transaction_id, plan):
    -        self.events.append(f"discard:{plan}")
    -
    -    async def stop_components(self, transaction_id, old_binding):
    -        self.events.append(f"stop:{old_binding}")
    -
    -    async def materialize_closed(self, transaction_id, plan):
    -        self.materialized += 1
    -        binding = f"binding:{plan}:{self.materialized}"
    -        self.events.append(f"materialize:{plan}")
    -        return binding
    -
    -    def finalize_components(self, transaction_id, binding):
    -        self.events.append(f"finalize:{binding}")
    -
    -    async def open_components(self, transaction_id, binding):
    -        self.events.append(f"open:{binding}")
    -
    -    def pause_components(self, binding):
    -        self.events.append(f"pause:{binding}")
    -
    -    async def restore_components(self, transaction_id, old_binding):
    -        self.events.append(f"restore:{old_binding}")
    -
    -    async def close_components(self, transaction_id, binding):
    -        self.events.append(f"close:{binding}")
    -
    -
    -def _stable_lease(revision: str):
    -    store = RuntimeSnapshotStore()
    -    snapshot = RuntimeSnapshotCompiler().compile({}, snapshot_revision=revision)
    -    store.install(snapshot)
    -    return store, store.lease(snapshot.snapshot_id)
    -
    -
    -def test_activity_identity_includes_exact_handler_generation() -> None:
    -    descriptor = SimpleNamespace(owner="probe")
    -    catalog = SimpleNamespace(identity="same", descriptors=(descriptor,))
    -    first = SimpleNamespace(
    -        background_job_catalog=catalog,
    -        generations={
    -            "probe": SimpleNamespace(
    -                generation_id="generation-a",
    -                source_revision="revision-a",
    -            )
    -        },
    -    )
    -    second = SimpleNamespace(
    -        background_job_catalog=catalog,
    -        generations={
    -            "probe": SimpleNamespace(
    -                generation_id="generation-b",
    -                source_revision="revision-b",
    -            )
    -        },
    -    )
    -
    -    assert PluginManager._activity_catalog_identity(cast(Any, first)) != (
    -        PluginManager._activity_catalog_identity(cast(Any, second))
    -    )
    -
    -
    -async def test_activity_host_prepare_is_pure_and_open_controls_admission() -> None:
    -    child = _RecordingChild()
    -    host = ActivityHost((child,))
    -    store, target_lease = _stable_lease("initial")
    -
    -    transaction = await host.prepare_transaction(target_lease)
    -
    -    assert child.events == ["prepare"]
    -    assert child.materialized == 0
    -    with pytest.raises(RuntimeError, match="admission"):
    -        host.acquire(target_lease)
    -
    -    await host.pause_and_drain(transaction)
    -    staged = await host.materialize_closed(transaction)
    -    assert not staged.admission_open
    -    host.finalize(transaction)
    -    assert host.active is staged
    -    assert not staged.admission_open
    -
    -    await host.open(transaction)
    -
    -    assert child.events[-2:] == [
    -        f"open:{staged.child_bindings['recording']}",
    -        f"finalize:{staged.child_bindings['recording']}",
    -    ]
    -
    -    source_lease = store.lease(staged.snapshot_id)
    -    lease = host.acquire(source_lease)
    -    assert lease.binding is staged
    -    await source_lease.release()
    -    await lease.release()
    -    assert not target_lease.active
    -    await host.close()
    -    await store.close()
    -
    -
    -async def test_activity_host_drain_waits_exact_old_in_flight_and_rollback_restores() -> (
    -    None
    -):
    -    child = _RecordingChild()
    -    host = ActivityHost((child,))
    -    old_store, old_target = _stable_lease("old")
    -    initial = await host.prepare_transaction(old_target)
    -    await host.pause_and_drain(initial)
    -    old_binding = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -    accepted_source = old_store.lease(old_binding.snapshot_id)
    -    accepted = host.acquire(accepted_source)
    -    await accepted_source.release()
    -
    -    new_store, new_target = _stable_lease("new")
    -    transaction = await host.prepare_transaction(new_target)
    -    drain = asyncio.create_task(host.pause_and_drain(transaction))
    -    await asyncio.sleep(0)
    -    assert not drain.done()
    -    assert not old_binding.admission_open
    -    rejected_snapshot_lease = old_store.lease(old_binding.snapshot_id)
    -    with pytest.raises(RuntimeError, match="admission"):
    -        host.acquire(rejected_snapshot_lease)
    -    await rejected_snapshot_lease.release()
    -
    -    await accepted.release()
    -    await drain
    -    staged = await host.materialize_closed(transaction)
    -    host.finalize(transaction)
    -    await host.rollback(transaction)
    -
    -    assert host.active is old_binding
    -    assert old_binding.admission_open
    -    assert not staged.admission_open
    -    assert not new_target.active
    -    assert any(event.startswith("stop:") for event in child.events)
    -    assert any(event.startswith("close:") for event in child.events)
    -    assert any(event.startswith("restore:") for event in child.events)
    -
    -    await host.close()
    -    await old_store.close()
    -    await new_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_materialize_failure_closes_partial_children() -> None:
    -    class _FailingChild(_RecordingChild):
    -        name = "failing"
    -
    -        async def materialize_closed(self, transaction_id, plan):
    -            raise RuntimeError("materialize failed")
    -
    -    first = _RecordingChild()
    -    failing = _FailingChild()
    -    host = ActivityHost((first, failing))
    -    store, target = _stable_lease("failure")
    -    transaction = await host.prepare_transaction(target)
    -    await host.pause_and_drain(transaction)
    -
    -    with pytest.raises(RuntimeError, match="materialize failed"):
    -        await host.materialize_closed(transaction)
    -
    -    assert any(event.startswith("close:") for event in first.events)
    -    await host.rollback(transaction)
    -    assert not target.active
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_prepare_failure_discards_prior_child_plans() -> None:
    -    class _PreparedChild(_RecordingChild):
    -        def discard_plan(self, transaction_id, plan):
    -            self.events.append(f"discard:{plan}")
    -
    -    class _FailingPrepareChild(_RecordingChild):
    -        name = "failing-prepare"
    -
    -        def prepare_components(self, transaction_id, target_lease, target_catalog):
    -            raise RuntimeError("prepare failed")
    -
    -    child = _PreparedChild()
    -    host = ActivityHost((child, _FailingPrepareChild()))
    -    store, target = _stable_lease("prepare-failure")
    -
    -    with pytest.raises(RuntimeError, match="prepare failed"):
    -        await host.prepare_transaction(target)
    -
    -    assert child.events == ["prepare", f"discard:{target.snapshot.snapshot_id}"]
    -    assert not target.active
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_retains_failed_rollback_for_exact_retry() -> None:
    -    class _FailCloseOnceChild(_RecordingChild):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.fail_close = True
    -
    -        async def close_components(self, transaction_id, binding):
    -            self.events.append(f"close:{binding}")
    -            if self.fail_close:
    -                self.fail_close = False
    -                raise RuntimeError("close failed")
    -
    -    child = _FailCloseOnceChild()
    -    host = ActivityHost((child,))
    -    store, target = _stable_lease("rollback-retry")
    -    transaction = await host.prepare_transaction(target)
    -    await host.pause_and_drain(transaction)
    -    _ = await host.materialize_closed(transaction)
    -
    -    with pytest.raises(RuntimeError, match="close failed"):
    -        await host.rollback(transaction)
    -    assert target.active
    -    await host.retry_recovery()
    -
    -    assert not target.active
    -    assert host.active is None
    -    assert (
    -        child.events.count("close:binding:" + transaction.target_snapshot_id + ":1")
    -        == 2
    -    )
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_committed_old_cleanup_failure_pauses_new_until_retry() -> None:
    -    class _FailOldCloseOnce(_RecordingChild):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.fail_close = False
    -
    -        async def close_components(self, transaction_id, binding):
    -            self.events.append(f"close:{binding}")
    -            if self.fail_close:
    -                self.fail_close = False
    -                raise RuntimeError("old cleanup failed")
    -
    -    child = _FailOldCloseOnce()
    -    host = ActivityHost((child,))
    -    old_store, old_target = _stable_lease("old-commit-cleanup")
    -    initial = await host.prepare_transaction(old_target)
    -    await host.pause_and_drain(initial)
    -    _ = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -
    -    new_store, new_target = _stable_lease("new-commit-cleanup")
    -    transaction = await host.prepare_transaction(new_target)
    -    await host.pause_and_drain(transaction)
    -    staged = await host.materialize_closed(transaction)
    -    host.finalize(transaction)
    -    child.fail_close = True
    -    with pytest.raises(RuntimeError, match="old cleanup failed"):
    -        await host.open(transaction)
    -
    -    assert host.active is staged
    -    assert not staged.admission_open
    -    assert child.events[-1] == f"pause:{staged.child_bindings['recording']}"
    -    await host.retry_recovery()
    -    assert staged.admission_open
    -    assert not new_target.active
    -    await host.close()
    -    await old_store.close()
    -    await new_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_committed_child_open_failure_pauses_new_until_retry() -> None:
    -    class _FailOpenOnce(_RecordingChild):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.fail_open = True
    -
    -        async def open_components(self, transaction_id, binding):
    -            self.events.append(f"open:{binding}")
    -            if self.fail_open:
    -                self.fail_open = False
    -                raise RuntimeError("open failed")
    -
    -    child = _FailOpenOnce()
    -    host = ActivityHost((child,))
    -    store, target = _stable_lease("open-retry")
    -    transaction = await host.prepare_transaction(target)
    -    await host.pause_and_drain(transaction)
    -    staged = await host.materialize_closed(transaction)
    -    host.finalize(transaction)
    -
    -    with pytest.raises(RuntimeError, match="open failed"):
    -        await host.open(transaction)
    -
    -    assert host.active is staged
    -    assert not staged.admission_open
    -    assert target.active
    -    await host.retry_recovery()
    -    assert staged.admission_open
    -    assert child.events.count(f"open:{staged.child_bindings['recording']}") == 2
    -    assert not target.active
    -    await host.close()
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_admission_stays_closed_while_child_open_is_blocked() -> None:
    -    class _BlockingOpen(_RecordingChild):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.entered = asyncio.Event()
    -            self.release = asyncio.Event()
    -
    -        async def open_components(self, transaction_id, binding):
    -            self.events.append(f"open:{binding}")
    -            self.entered.set()
    -            await self.release.wait()
    -
    -    child = _BlockingOpen()
    -    host = ActivityHost((child,))
    -    store, target = _stable_lease("blocked-open")
    -    transaction = await host.prepare_transaction(target)
    -    await host.pause_and_drain(transaction)
    -    staged = await host.materialize_closed(transaction)
    -    host.finalize(transaction)
    -    opening = asyncio.create_task(host.open(transaction))
    -    await child.entered.wait()
    -
    -    assert host.active is staged
    -    assert not staged.admission_open
    -    rejected = store.lease(staged.snapshot_id)
    -    with pytest.raises(RuntimeError, match="admission"):
    -        host.acquire(rejected)
    -    await rejected.release()
    -
    -    child.release.set()
    -    await opening
    -    assert staged.admission_open
    -    await host.close()
    -    await store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_restores_child_that_changed_before_stop_failed() -> None:
    -    class _StopAfterChangeChild(_RecordingChild):
    -        async def stop_components(self, transaction_id, old_binding):
    -            self.events.append(f"stop:{old_binding}")
    -            raise RuntimeError("stop failed after change")
    -
    -    child = _StopAfterChangeChild()
    -    host = ActivityHost((child,))
    -    old_store, old_target = _stable_lease("old-stop-failure")
    -    initial = await host.prepare_transaction(old_target)
    -    await host.pause_and_drain(initial)
    -    old_binding = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -
    -    new_store, new_target = _stable_lease("new-stop-failure")
    -    transaction = await host.prepare_transaction(new_target)
    -    with pytest.raises(RuntimeError, match="stop failed after change"):
    -        await host.pause_and_drain(transaction)
    -    await host.rollback(transaction)
    -
    -    assert child.events[-2:] == [
    -        f"stop:{old_binding.child_bindings['recording']}",
    -        f"restore:{old_binding.child_bindings['recording']}",
    -    ]
    -    assert host.active is old_binding
    -    assert old_binding.admission_open
    -    await host.close()
    -    await old_store.close()
    -    await new_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_retry_preserves_selected_rollback_direction() -> None:
    -    class _CloseOnceChild(_RecordingChild):
    -        def __init__(self) -> None:
    -            super().__init__()
    -            self.fail_next_close = False
    -
    -        async def close_components(self, transaction_id, binding):
    -            self.events.append(f"close:{binding}")
    -            if self.fail_next_close:
    -                self.fail_next_close = False
    -                raise RuntimeError("staged close failed")
    -
    -    child = _CloseOnceChild()
    -    host = ActivityHost((child,))
    -    old_store, old_target = _stable_lease("rollback-direction-old")
    -    initial = await host.prepare_transaction(old_target)
    -    await host.pause_and_drain(initial)
    -    old_binding = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -
    -    new_store, new_target = _stable_lease("rollback-direction-new")
    -    transaction = await host.prepare_transaction(new_target)
    -    await host.pause_and_drain(transaction)
    -    await host.materialize_closed(transaction)
    -    host.finalize(transaction)
    -    child.fail_next_close = True
    -    with pytest.raises(RuntimeError, match="staged close failed"):
    -        await host.rollback(transaction)
    -
    -    await host.retry_recovery()
    -
    -    assert host.active is old_binding
    -    assert old_binding.admission_open
    -    assert not new_target.active
    -    await host.close()
    -    await old_store.close()
    -    await new_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_host_cancelled_drain_can_rollback_and_reopen_old() -> None:
    -    child = _RecordingChild()
    -    host = ActivityHost((child,))
    -    old_store, old_target = _stable_lease("old-cancel")
    -    initial = await host.prepare_transaction(old_target)
    -    await host.pause_and_drain(initial)
    -    old_binding = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -    accepted_source = old_store.lease(old_binding.snapshot_id)
    -    accepted = host.acquire(accepted_source)
    -    await accepted_source.release()
    -
    -    new_store, new_target = _stable_lease("new-cancel")
    -    transaction = await host.prepare_transaction(new_target)
    -    drain = asyncio.create_task(host.pause_and_drain(transaction))
    -    await asyncio.sleep(0)
    -    drain.cancel()
    -    with pytest.raises(asyncio.CancelledError):
    -        await drain
    -
    -    await host.rollback(transaction)
    -
    -    assert host.active is old_binding
    -    assert old_binding.admission_open
    -    await accepted.release()
    -    await host.close()
    -    await old_store.close()
    -    await new_store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_activity_admission_lease_pins_exact_snapshot_until_release() -> None:
    -    drained: list[str] = []
    -
    -    async def on_drained(snapshot) -> None:
    -        drained.append(snapshot.snapshot_id)
    -
    -    child = _RecordingChild()
    -    host = ActivityHost((child,))
    -    store = RuntimeSnapshotStore(on_drained=on_drained)
    -    compiler = RuntimeSnapshotCompiler()
    -    old = compiler.compile({}, snapshot_revision="pin-old")
    -    new = compiler.compile({}, snapshot_revision="pin-new")
    -    store.install(old)
    -    initial = await host.prepare_transaction(store.lease(old.snapshot_id))
    -    await host.pause_and_drain(initial)
    -    binding = await host.materialize_closed(initial)
    -    host.finalize(initial)
    -    await host.open(initial)
    -    source_lease = store.lease(old.snapshot_id)
    -    accepted = host.acquire(source_lease)
    -    await source_lease.release()
    -
    -    await store.commit(store.begin_publish(new))
    -    await asyncio.sleep(0)
    -
    -    assert drained == []
    -    assert old.lease_count == 1
    -    await accepted.release()
    -    await store.retry_drains()
    -    assert drained == [old.snapshot_id]
    -    await host.close()
    -    await store.close()
    diff --git a/tests/test_plugin_hot_reload.py b/tests/test_plugin_hot_reload.py
    index e1e1f2f87..f1080959a 100644
    --- a/tests/test_plugin_hot_reload.py
    +++ b/tests/test_plugin_hot_reload.py
    @@ -1750,7 +1750,10 @@ def receive_restart() -> int:
                     )
                     == 1012
                 )
    -            await asyncio.wait_for(publication, timeout=5)
    +
    +        # Finish the close handshake so middleware can release its snapshot
    +        # lease; publication can only drain after that lifecycle boundary.
    +        await asyncio.wait_for(publication, timeout=5)
     
         await manager.snapshot_store.retry_drains()
         await manager.terminate_all()
    diff --git a/tests/test_plugin_interaction_undo.py b/tests/test_plugin_interaction_undo.py
    deleted file mode 100644
    index 50535b650..000000000
    --- a/tests/test_plugin_interaction_undo.py
    +++ /dev/null
    @@ -1,122 +0,0 @@
    -from __future__ import annotations
    -
    -from datetime import UTC, datetime
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.plugin_composition import InteractionUndoService
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.interaction_undo import InteractionUndoCoordinator
    -from agent.plugins.manager import PluginManager
    -from bus.event_bus import EventBus
    -from session.manager import SessionManager
    -
    -
    -def _seed_interaction(
    -    manager: SessionManager,
    -    *,
    -    session_key: str = "cli:undo",
    -    turn_id: str = "turn:undo",
    -) -> tuple[str, ...]:
    -    now = datetime.now(UTC).isoformat()
    -    rows = manager.control_store.persist_session(
    -        session_key,
    -        created_at=now,
    -        updated_at=now,
    -        metadata={},
    -        messages=[
    -            {
    -                "role": "user",
    -                "content": "question",
    -                "timestamp": now,
    -                "extra": {
    -                    "control_turn_id": turn_id,
    -                    "turn_input_ordinal": 0,
    -                },
    -            },
    -            {
    -                "role": "assistant",
    -                "content": "answer",
    -                "timestamp": now,
    -                "extra": {
    -                    "control_turn_id": turn_id,
    -                    "turn_terminal": True,
    -                    "turn_input_count": 1,
    -                },
    -            },
    -        ],
    -    )
    -    return tuple(str(row["id"]) for row in rows)
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_interaction_undo_has_no_destructive_owner() -> None:
    -    service = InteractionUndoService.candidate_validation()
    -
    -    with pytest.raises(RuntimeError, match="candidate 验证期禁止"):
    -        await service.undo_latest("cli:undo")
    -
    -
    -@pytest.mark.asyncio
    -async def test_undo_runs_bound_source_fence_and_invalidates_session(tmp_path) -> None:
    -    sessions = SessionManager(tmp_path)
    -    message_ids = _seed_interaction(sessions)
    -    _ = sessions.get_existing("cli:undo")
    -    coordinator = InteractionUndoCoordinator(cast(Any, sessions))
    -    service = InteractionUndoService(coordinator.undo_latest)
    -    fenced: list[str] = []
    -
    -    async def fence(control_turn_id, delete_source):
    -        fenced.append(control_turn_id)
    -        return delete_source()
    -
    -    cleanup = service.bind_source_fence(fence)
    -    result = await service.undo_latest("cli:undo")
    -
    -    assert result is not None
    -    assert result.message_ids == message_ids
    -    assert result.reconciliation_pending is False
    -    assert fenced == ["turn:undo"]
    -    assert sessions.get_existing("cli:undo").messages == []
    -    cleanup()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manager_keeps_external_plugin_undo_contract(tmp_path) -> None:
    -    workspace = tmp_path / "workspace"
    -    sessions = SessionManager(workspace)
    -    message_ids = _seed_interaction(sessions)
    -    plugin_dir = tmp_path / "plugins" / "plugin_undo"
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import INTERACTION_UNDO\n"
    -        "api_version = 3\n"
    -        "name = 'plugin_undo'\n"
    -        "version = '2.0.0'\n"
    -        "inject = (INTERACTION_UNDO,)\n"
    -        "service = None\n"
    -        "async def apply(ctx, config):\n"
    -        "    global service\n"
    -        "    service = ctx.require(INTERACTION_UNDO)\n",
    -        encoding="utf-8",
    -    )
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        session_manager=sessions,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    try:
    -        await manager.load_all()
    -        generation = manager.generation("plugin_undo")
    -        assert generation is not None
    -        assert isinstance(generation.instance, ComposablePlugin)
    -        service = generation.instance.module.service
    -        assert isinstance(service, InteractionUndoService)
    -        result = await service.undo_latest("cli:undo")
    -        assert result is not None and result.message_ids == message_ids
    -    finally:
    -        await manager.terminate_all()
    diff --git a/tests/test_plugin_job_outcome_ledger.py b/tests/test_plugin_job_outcome_ledger.py
    deleted file mode 100644
    index f802fcafb..000000000
    --- a/tests/test_plugin_job_outcome_ledger.py
    +++ /dev/null
    @@ -1,365 +0,0 @@
    -"""真实 SQLite tests for the C21 JobOutcomeLedger state contract."""
    -
    -from __future__ import annotations
    -
    -import sqlite3
    -from contextlib import closing
    -from dataclasses import replace
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugins.job_outcome_ledger import (
    -    JobOutcomeIdentity,
    -    JobOutcomeIdentityError,
    -    JobOutcomeLedger,
    -    JobOutcomePhase,
    -    JobOutcomeState,
    -    JobOutcomeTransitionError,
    -    ProgrammaticTurnState,
    -)
    -
    -
    -def _identity(
    -    *,
    -    invocation_id: str = "invocation-1",
    -    event_id: str | None = "event-1",
    -    interval_bucket: str | None = None,
    -    snapshot_id: str = "snapshot-1",
    -    plugin_generation_id: str = "plugin-generation-1",
    -    model_generation_id: str = "model-generation-1",
    -) -> JobOutcomeIdentity:
    -    return JobOutcomeIdentity(
    -        plugin_id="plugin.example",
    -        job_name="merge_pending",
    -        invocation_id=invocation_id,
    -        event_id=event_id,
    -        interval_bucket=interval_bucket,
    -        snapshot_id=snapshot_id,
    -        plugin_generation_id=plugin_generation_id,
    -        model_generation_id=model_generation_id,
    -        artifact_identity="artifact-sha256:abc",
    -        source_revision="source-revision-1",
    -        handler_export="jobs.merge_pending",
    -        lifecycle_revision="lifecycle-3",
    -        api_revision="plugin-api-v3",
    -    )
    -
    -
    -def test_admission_persists_exact_identity_and_deduplicates_event(tmp_path: Path) -> None:
    -    path = tmp_path / "runtime" / "plugin-jobs" / "outcomes.sqlite"
    -    ledger = JobOutcomeLedger(path)
    -
    -    first = ledger.admit(_identity())
    -    duplicate = ledger.admit(
    -        _identity(
    -            invocation_id="invocation-from-repeated-delivery",
    -            snapshot_id="new-snapshot-must-not-rerun",
    -            plugin_generation_id="new-plugin-generation",
    -            model_generation_id="new-model-generation",
    -        )
    -    )
    -
    -    assert first.state is JobOutcomeState.QUEUED
    -    assert first.phase is JobOutcomePhase.HANDLER
    -    assert duplicate == first
    -    assert ledger.get("invocation-from-repeated-delivery") is None
    -    assert len(ledger.list_all()) == 1
    -
    -    with closing(sqlite3.connect(path)) as connection:
    -        row = connection.execute(
    -            """
    -            SELECT semantic_job_id, invocation_id, event_id, snapshot_id,
    -                   plugin_generation_id, model_generation_id, artifact_identity,
    -                   source_revision, handler_export, lifecycle_revision, api_revision
    -            FROM job_outcomes
    -            """
    -        ).fetchone()
    -    assert row == (
    -        "plugin.example:merge_pending",
    -        "invocation-1",
    -        "event-1",
    -        "snapshot-1",
    -        "plugin-generation-1",
    -        "model-generation-1",
    -        "artifact-sha256:abc",
    -        "source-revision-1",
    -        "jobs.merge_pending",
    -        "lifecycle-3",
    -        "plugin-api-v3",
    -    )
    -    ledger.integrity_check()
    -
    -
    -def test_programmatic_turn_receipt_survives_process_reopen(tmp_path: Path) -> None:
    -    path = tmp_path / "outcomes.sqlite"
    -    ledger = JobOutcomeLedger(path)
    -    admitted = ledger.admit(_identity())
    -    _ = ledger.transition(admitted.invocation_id, JobOutcomeState.RUNNING)
    -    submitting = ledger.begin_programmatic_turn(admitted.invocation_id)
    -    assert submitting.programmatic_turn_state is ProgrammaticTurnState.SUBMITTING
    -    ledger.close()
    -
    -    reopened = JobOutcomeLedger(path)
    -    retained = reopened.require(admitted.invocation_id)
    -    assert retained.programmatic_turn_state is ProgrammaticTurnState.SUBMITTING
    -    assert retained.programmatic_turn_id is None
    -    committed = reopened.commit_programmatic_turn(admitted.invocation_id, "turn-1")
    -    assert committed.programmatic_turn_state is ProgrammaticTurnState.ADMITTED
    -    assert committed.programmatic_turn_id == "turn-1"
    -
    -
    -def test_invocation_id_cannot_be_reused_for_another_event(tmp_path: Path) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    ledger.admit(_identity())
    -
    -    with pytest.raises(JobOutcomeIdentityError):
    -        ledger.admit(
    -            _identity(
    -                event_id="another-event",
    -                invocation_id="invocation-1",
    -            )
    -        )
    -    assert len(ledger.list_all()) == 1
    -
    -
    -def test_legal_transitions_increment_retry_attempt_and_terminal_once(
    -    tmp_path: Path,
    -) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    ledger.admit(_identity())
    -
    -    running = ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    pending = ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.RETRY_PENDING,
    -        phase=JobOutcomePhase.HANDLER,
    -        error="provider unavailable",
    -    )
    -    retried = ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    failed = ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.FAILED,
    -        error="retry exhausted",
    -    )
    -
    -    assert running.attempt == 1
    -    assert pending.phase is JobOutcomePhase.HANDLER
    -    assert retried.attempt == 2
    -    assert retried.error is None
    -    assert failed.state is JobOutcomeState.FAILED
    -    assert failed.attempt == 2
    -    with pytest.raises(JobOutcomeTransitionError):
    -        ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -
    -
    -def test_provider_retry_phase_is_reachable(tmp_path: Path) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    ledger.admit(_identity())
    -    ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -
    -    pending = ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.RETRY_PENDING,
    -        phase=JobOutcomePhase.PROVIDER,
    -        error="provider request failed before domain effect",
    -    )
    -
    -    assert pending.state is JobOutcomeState.RETRY_PENDING
    -    assert pending.phase is JobOutcomePhase.PROVIDER
    -    assert pending.error == "provider request failed before domain effect"
    -
    -
    -def test_running_binds_actual_model_generation_once(tmp_path: Path) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    pending_identity = replace(
    -        _identity(),
    -        model_generation_id="execution-pending",
    -    )
    -    ledger.admit(pending_identity)
    -
    -    running = ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.RUNNING,
    -        model_generation_id="model-generation-2",
    -    )
    -    duplicate = ledger.admit(pending_identity)
    -
    -    assert running.model_generation_id == "model-generation-2"
    -    assert duplicate == running
    -
    -    ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.RETRY_PENDING,
    -        error="provider unavailable",
    -    )
    -    with pytest.raises(JobOutcomeIdentityError, match="model generation"):
    -        ledger.transition(
    -            "invocation-1",
    -            JobOutcomeState.RUNNING,
    -            model_generation_id="model-generation-3",
    -        )
    -
    -
    -def test_outcome_field_invariants_fail_loud(tmp_path: Path) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    ledger.admit(_identity())
    -
    -    with pytest.raises(JobOutcomeTransitionError, match="running.*error"):
    -        ledger.transition("invocation-1", JobOutcomeState.RUNNING, error="stale")
    -    with pytest.raises(JobOutcomeTransitionError, match="running.*terminal"):
    -        ledger.transition(
    -            "invocation-1",
    -            JobOutcomeState.RUNNING,
    -            terminal_result_digest="stale-digest",
    -        )
    -    ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -
    -    with pytest.raises(JobOutcomeTransitionError, match="retry_pending.*error"):
    -        ledger.transition("invocation-1", JobOutcomeState.RETRY_PENDING)
    -    with pytest.raises(JobOutcomeTransitionError, match="failed.*error"):
    -        ledger.transition("invocation-1", JobOutcomeState.FAILED)
    -    with pytest.raises(JobOutcomeTransitionError, match="succeeded.*terminal"):
    -        ledger.transition("invocation-1", JobOutcomeState.SUCCEEDED)
    -    with pytest.raises(JobOutcomeTransitionError, match="failed.*terminal"):
    -        ledger.transition(
    -            "invocation-1",
    -            JobOutcomeState.FAILED,
    -            error="failed",
    -            terminal_result_digest="unexpected",
    -        )
    -
    -    assert ledger.require("invocation-1").state is JobOutcomeState.RUNNING
    -
    -
    -def test_documents_phase_is_forward_recovery_only(tmp_path: Path) -> None:
    -    ledger = JobOutcomeLedger(tmp_path / "outcomes.sqlite")
    -    ledger.admit(_identity())
    -    ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.RETRY_PENDING,
    -        phase=JobOutcomePhase.DOCUMENTS,
    -        error="document commit interrupted",
    -    )
    -
    -    with pytest.raises(JobOutcomeTransitionError):
    -        ledger.transition("invocation-1", JobOutcomeState.CANCELLED)
    -    with pytest.raises(JobOutcomeTransitionError):
    -        ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    with pytest.raises(JobOutcomeTransitionError):
    -        ledger.transition("invocation-1", JobOutcomeState.FAILED)
    -
    -    succeeded = ledger.transition(
    -        "invocation-1",
    -        JobOutcomeState.SUCCEEDED,
    -        terminal_result_digest="result-sha256:done",
    -    )
    -    assert succeeded.phase is JobOutcomePhase.DOCUMENTS
    -    assert succeeded.result_digest == "result-sha256:done"
    -    assert succeeded.terminal
    -
    -
    -def test_restart_reads_pending_records_with_exact_identity(tmp_path: Path) -> None:
    -    path = tmp_path / "outcomes.sqlite"
    -    first_ledger = JobOutcomeLedger(path)
    -    first_ledger.admit(_identity(interval_bucket="2026-08-17T03:00Z", event_id=None))
    -    first_ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    first_ledger.close()
    -
    -    restarted = JobOutcomeLedger(path)
    -    pending = restarted.list_pending()
    -
    -    assert len(pending) == 1
    -    record = pending[0]
    -    assert record.invocation_id == "invocation-1"
    -    assert record.semantic_job_id == "plugin.example:merge_pending"
    -    assert record.interval_bucket == "2026-08-17T03:00Z"
    -    assert record.snapshot_id == "snapshot-1"
    -    assert record.plugin_generation_id == "plugin-generation-1"
    -    assert record.model_generation_id == "model-generation-1"
    -    assert record.state is JobOutcomeState.RUNNING
    -
    -
    -def test_restart_reads_pending_event_payload_without_changing_binding(tmp_path: Path) -> None:
    -    path = tmp_path / "outcomes.sqlite"
    -    payload = {
    -        "event_id": "drift:durable-1",
    -        "session_key": "session-1",
    -        "skill_name": "explore-curiosity",
    -        "status": "completed",
    -        "briefing": "done",
    -        "message_result": "silent",
    -        "timestamp": "2026-08-17T03:00:00+00:00",
    -    }
    -    identity = _identity(event_id="drift:durable-1")
    -    first_ledger = JobOutcomeLedger(path)
    -    first_ledger.admit(identity=identity, event_payload=payload)
    -    first_ledger.transition("invocation-1", JobOutcomeState.RUNNING)
    -    first_ledger.close()
    -
    -    restarted = JobOutcomeLedger(path)
    -    pending = restarted.list_pending()
    -
    -    assert len(pending) == 1
    -    record = pending[0]
    -    assert record.event_id == "drift:durable-1"
    -    assert dict(record.event_payload or {}) == payload
    -    assert dict(record.identity().event_payload or {}) == payload
    -    assert record.snapshot_id == "snapshot-1"
    -    assert record.plugin_generation_id == "plugin-generation-1"
    -
    -
    -def test_schema_migrates_old_outcome_table_without_payload_column(tmp_path: Path) -> None:
    -    path = tmp_path / "outcomes.sqlite"
    -    ledger = JobOutcomeLedger(path)
    -    ledger.close()
    -    with closing(sqlite3.connect(path)) as connection:
    -        connection.execute("ALTER TABLE job_outcomes DROP COLUMN event_payload_json")
    -        connection.execute("ALTER TABLE job_outcomes DROP COLUMN programmatic_turn_state")
    -        connection.execute("ALTER TABLE job_outcomes DROP COLUMN programmatic_turn_id")
    -        connection.execute("PRAGMA user_version = 1")
    -        connection.commit()
    -
    -    migrated = JobOutcomeLedger(path)
    -    with closing(sqlite3.connect(path)) as connection:
    -        columns = {
    -            row[1]
    -            for row in connection.execute("PRAGMA table_info(job_outcomes)")
    -        }
    -        version = int(connection.execute("PRAGMA user_version").fetchone()[0])
    -    assert "event_payload_json" in columns
    -    assert "programmatic_turn_state" in columns
    -    assert "programmatic_turn_id" in columns
    -    assert version == 3
    -    assert migrated.list_all() == ()
    -
    -
    -def test_transaction_rolls_back_real_sqlite_failure(tmp_path: Path) -> None:
    -    path = tmp_path / "outcomes.sqlite"
    -    ledger = JobOutcomeLedger(path)
    -    with closing(sqlite3.connect(path)) as connection:
    -        connection.execute(
    -            """
    -            CREATE TRIGGER reject_outcome_insert
    -            BEFORE INSERT ON job_outcomes
    -            BEGIN
    -                SELECT RAISE(ABORT, 'durable insert rejected');
    -            END
    -            """
    -        )
    -        connection.commit()
    -
    -    with pytest.raises(sqlite3.IntegrityError, match="durable insert rejected"):
    -        ledger.admit(_identity())
    -
    -    assert ledger.list_all() == ()
    -
    -
    -def test_identity_requires_one_trigger_and_matching_semantic_key() -> None:
    -    with pytest.raises(ValueError, match="恰好提供"):
    -        _identity(event_id=None, interval_bucket=None)
    -    with pytest.raises(ValueError, match="恰好提供"):
    -        _identity(interval_bucket="bucket")
    -    with pytest.raises(JobOutcomeIdentityError):
    -        replace(_identity(), semantic_job_id="wrong-owner:job")
    diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py
    deleted file mode 100644
    index f268274c6..000000000
    --- a/tests/test_plugin_manager.py
    +++ /dev/null
    @@ -1,528 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import json
    -import os
    -import shutil
    -import sys
    -import tempfile
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -# 预热 agent.core 导入链,避免 agent.lifecycle.types 触发循环导入
    -from agent.core.passive_turn import ContextStore as _  # noqa: F401
    -from agent.config_models import Config
    -from agent.plugins.artifacts import ArtifactPointer, write_pointers
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.scope import PluginScope
    -from bus.event_bus import EventBus
    -
    -TEST_PLUGIN_HOME = Path(tempfile.gettempdir()) / f"akasic-plugin-tests-{os.getpid()}"
    -
    -
    -@pytest.fixture(autouse=True)
    -def _clean_plugin_home():
    -    """Clear the isolated plugin home around each test."""
    -
    -    shutil.rmtree(TEST_PLUGIN_HOME, ignore_errors=True)
    -    yield
    -    shutil.rmtree(TEST_PLUGIN_HOME, ignore_errors=True)
    -
    -
    -def _write_v3_plugin(
    -    plugin_dir: Path,
    -    *,
    -    name: str | None = None,
    -    version: str = "1.0.0",
    -    source: str = "def apply(ctx, config):\n    return None\n",
    -    static_manifest: bool = True,
    -) -> Path:
    -    """Write one v3 namespace plugin and its static artifact identity."""
    -
    -    # 1. Write the exact module-level namespace consumed by ComposablePlugin.
    -    plugin_name = plugin_dir.name if name is None else name
    -    plugin_dir.mkdir(parents=True, exist_ok=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        f"api_version = 3\nname = {plugin_name!r}\nversion = {version!r}\n\n{source}",
    -        encoding="utf-8",
    -    )
    -
    -    # 2. Installed artifacts also need an import-free static identity manifest.
    -    if static_manifest:
    -        (plugin_dir / "akashic.plugin.toml").write_text(
    -            "schema_version = 1\n"
    -            f"name = {json.dumps(plugin_name)}\n"
    -            f"version = {json.dumps(version)}\n"
    -            "api_version = 3\n"
    -            'entrypoint = "plugin.py"\n',
    -            encoding="utf-8",
    -        )
    -    return plugin_dir
    -
    -
    -def _write_installed_v3_plugin(
    -    cache_root: Path,
    -    *,
    -    marketplace: str,
    -    name: str,
    -    version: str = "1.0.0",
    -    source: str = "def apply(ctx, config):\n    return None\n",
    -) -> Path:
    -    plugin_base = cache_root / marketplace / name
    -    artifact_id = f"{version}-test"
    -    plugin_root = _write_v3_plugin(
    -        plugin_base / ".artifacts" / artifact_id,
    -        name=name,
    -        version=version,
    -        source=source,
    -    )
    -    pointer = ArtifactPointer(f".artifacts/{artifact_id}")
    -    _ = write_pointers(plugin_base, stable=pointer, latest=pointer)
    -    return plugin_root
    -
    -
    -def _make_manager(
    -    plugin_dirs: list[Path],
    -    *,
    -    event_bus: EventBus,
    -    workspace: Path | None = None,
    -    installed_cache_root: Path | None = None,
    -) -> PluginManager:
    -    return PluginManager(
    -        plugin_dirs=plugin_dirs,
    -        event_bus=event_bus,
    -        workspace=workspace or TEST_PLUGIN_HOME / "workspace",
    -        installed_cache_root=installed_cache_root or TEST_PLUGIN_HOME / "cache",
    -    )
    -
    -
    -async def test_load_hello_plugin(tmp_path: Path):
    -    plugin_root = tmp_path / "plugins"
    -    _write_v3_plugin(plugin_root / "hello", name="hello", version="0.1.0")
    -    mgr = _make_manager(
    -        [plugin_root],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        assert mgr.loaded_count == 1
    -        assert {item["name"] for item in mgr.discover()} == {"hello"}
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_duplicate_plugin_name_first_wins(tmp_path: Path):
    -    first_root = tmp_path / "first"
    -    second_root = tmp_path / "second"
    -    _write_v3_plugin(first_root / "duplicate", name="duplicate")
    -    _write_v3_plugin(second_root / "duplicate", name="duplicate")
    -    mgr = _make_manager(
    -        [first_root, second_root],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        assert mgr.loaded_count == len({item["name"] for item in mgr.discover()})
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_installed_plugin_shadows_builtin_with_same_name(tmp_path: Path):
    -    builtin_root = tmp_path / "plugins"
    -    installed_root = tmp_path / "cache"
    -    _write_v3_plugin(builtin_root / "shadow", name="shadow")
    -    _write_installed_v3_plugin(
    -        installed_root,
    -        marketplace="github",
    -        name="shadow",
    -        version="0.1.0",
    -    )
    -    mgr = PluginManager(
    -        plugin_dirs=[builtin_root],
    -        installed_cache_root=installed_root,
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    try:
    -        assert [item["source_type"] for item in mgr.discover()] == ["installed"]
    -        await mgr.load_all()
    -        assert [plugin.plugin_id for plugin in mgr.active_plugins()] == [
    -            "shadow@github"
    -        ]
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_event_subscription_can_be_closed():
    -    bus = EventBus()
    -    called: list[str] = []
    -    subscription = bus.on(str, lambda event: called.append(event))
    -
    -    await bus.fanout("first")
    -    subscription.close()
    -    await bus.fanout("second")
    -
    -    assert called == ["first"]
    -    assert bus.handler_count() == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_observe_keeps_current_handler_snapshot():
    -    bus = EventBus()
    -    called: list[str] = []
    -    first = None
    -
    -    def close_first(_event: str) -> None:
    -        called.append("first")
    -        assert first is not None
    -        first.close()
    -
    -    first = bus.on(str, close_first)
    -    _ = bus.on(str, lambda _event: called.append("second"))
    -
    -    await bus.observe("event")
    -
    -    assert called == ["first", "second"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_scope_cleans_in_reverse_order_after_failure():
    -    scope = PluginScope("scope-test")
    -    cleaned: list[str] = []
    -
    -    def fail() -> None:
    -        cleaned.append("fail")
    -        raise RuntimeError("cleanup failed")
    -
    -    scope.defer("first", lambda: cleaned.append("first"))
    -    scope.defer("failure", fail)
    -    scope.defer("last", lambda: cleaned.append("last"))
    -
    -    failures = await scope.aclose()
    -
    -    assert cleaned == ["last", "fail", "first"]
    -    assert [(item.resource, item.error) for item in failures] == [
    -        ("failure", "cleanup failed")
    -    ]
    -    assert scope.resource_count == 0
    -
    -
    -def test_plugin_scope_rejects_non_callable_cleanup() -> None:
    -    scope = PluginScope("invalid-cleanup")
    -    cleanup: Any = None
    -
    -    with pytest.raises(
    -        TypeError,
    -        match="插件清理动作不可调用: invalid-cleanup:broken",
    -    ):
    -        scope.defer("broken", cleanup)
    -
    -    assert scope.resource_count == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_scope_continues_after_cancelled_cleanup():
    -    scope = PluginScope("cancelled-cleanup")
    -    cleaned: list[str] = []
    -
    -    def cancelled() -> None:
    -        raise asyncio.CancelledError
    -
    -    scope.defer("last", lambda: cleaned.append("last"))
    -    scope.defer("cancelled", cancelled)
    -    scope.defer("first", lambda: cleaned.append("first"))
    -
    -    failures = await scope.aclose()
    -
    -    assert cleaned == ["first", "last"]
    -    assert [failure.resource for failure in failures] == ["cancelled"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_scope_finishes_cleanup_after_external_cancellation():
    -    scope = PluginScope("cancelled-close")
    -    entered = asyncio.Event()
    -    release = asyncio.Event()
    -    cancelled_inside_cleanup = False
    -    cleaned: list[str] = []
    -
    -    async def cleanup() -> None:
    -        nonlocal cancelled_inside_cleanup
    -        entered.set()
    -        try:
    -            await release.wait()
    -        except asyncio.CancelledError:
    -            cancelled_inside_cleanup = True
    -            await release.wait()
    -
    -    scope.defer("slow", cleanup)
    -    scope.defer("marker", lambda: cleaned.append("marker"))
    -    closing = asyncio.create_task(scope.aclose())
    -    await entered.wait()
    -    closing.cancel()
    -    release.set()
    -
    -    with pytest.raises(asyncio.CancelledError):
    -        await closing
    -
    -    assert cancelled_inside_cleanup is False
    -    assert cleaned == ["marker"]
    -    assert scope.resource_count == 0
    -    assert await scope.aclose() == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_closed_plugin_scope_does_not_accept_cleanup():
    -    scope = PluginScope("closed")
    -    _ = await scope.aclose()
    -
    -    with pytest.raises(RuntimeError, match="作用域已关闭"):
    -        scope.defer("late", lambda: None)
    -
    -
    -@pytest.mark.asyncio
    -async def test_plugin_manager_scope_cleans_v3_resources(tmp_path: Path):
    -    plugin_dir = _write_v3_plugin(
    -        tmp_path / "plugins" / "scoped",
    -        source=(
    -            "import asyncio\n\n"
    -            "task = None\n\n"
    -            "async def apply(ctx, config):\n"
    -            "    global task\n"
    -            '    task = await ctx.spawn(asyncio.Event().wait(), name="scoped-worker")\n'
    -        ),
    -    )
    -    manager = _make_manager(
    -        [plugin_dir.parent],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -    module = sys.modules["akasic_plugin_plugins_scoped"]
    -    assert module.task is not None
    -    assert not module.task.done()
    -
    -    await manager.terminate_all()
    -
    -    assert module.task.done()
    -    assert manager.loaded_count == 0
    -    assert manager.cleanup_failures == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_active_plugins_exposes_v3_metadata(tmp_path: Path):
    -    plugin_dir = _write_v3_plugin(
    -        tmp_path / "plugins" / "manifested",
    -        name="manifested",
    -        version="1.0.0",
    -        source=(
    -            'desc = "v3 declaration"\n'
    -            'author = "tester"\n'
    -            'skill_roots = ("skills",)\n\n'
    -            "def apply(ctx, config):\n"
    -            "    return None\n"
    -        ),
    -    )
    -    (plugin_dir / "skills").mkdir()
    -    mgr = _make_manager(
    -        [plugin_dir.parent],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        active = mgr.active_plugins()
    -        assert len(active) == 1
    -        assert active[0].plugin_id == "manifested"
    -        assert active[0].plugin_dir == plugin_dir
    -        assert active[0].manifest == {
    -            "name": "manifested",
    -            "version": "1.0.0",
    -            "desc": "v3 declaration",
    -            "author": "tester",
    -        }
    -        assert active[0].skill_roots == (plugin_dir / "skills",)
    -        generation = mgr.generation("manifested")
    -        assert generation is not None and generation.static_manifest is not None
    -        assert generation.static_manifest.api_version == 3
    -        assert generation.static_manifest.entrypoint == "plugin.py"
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_loads_installed_v3_plugin(tmp_path: Path):
    -    cache_root = tmp_path / "cache"
    -    plugin_root = _write_installed_v3_plugin(
    -        cache_root,
    -        marketplace="lab",
    -        name="feed",
    -        version="1.0.0",
    -        source=(
    -            'skill_roots = ("skills",)\n\n'
    -            "def apply(ctx, config):\n"
    -            "    return None\n"
    -        ),
    -    )
    -    (plugin_root / "skills" / "feed-manage").mkdir(parents=True)
    -    (plugin_root / "skills" / "feed-manage" / "SKILL.md").write_text(
    -        "---\nname: feed-manage\ndescription: feed\n---\nbody\n",
    -        encoding="utf-8",
    -    )
    -    mgr = PluginManager(
    -        plugin_dirs=[],
    -        installed_cache_root=cache_root,
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        active = mgr.active_plugins()
    -        assert len(active) == 1
    -        assert active[0].plugin_id == "feed@lab"
    -        assert active[0].skill_roots == (plugin_root / "skills",)
    -        assert mgr.loaded_count == 1
    -        generation = mgr.generation("feed@lab")
    -        assert generation is not None and generation.static_manifest is not None
    -        assert generation.static_manifest.name == "feed"
    -        assert generation.static_manifest.api_version == 3
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_sync_manifest_covers_builtin_and_installed_plugins(tmp_path: Path):
    -    builtin_root = tmp_path / "plugins"
    -    _write_v3_plugin(builtin_root / "hello", name="hello", version="0.1.0")
    -
    -    cache_root = tmp_path / "cache"
    -    installed_root = _write_installed_v3_plugin(
    -        cache_root,
    -        marketplace="lab",
    -        name="feed",
    -        version="1.0.0",
    -        source=(
    -            'skill_roots = ("skills",)\n\n'
    -            "def apply(ctx, config):\n"
    -            "    return None\n"
    -        ),
    -    )
    -    (installed_root / "skills" / "feed-manage").mkdir(parents=True)
    -    (installed_root / "skills" / "feed-manage" / "SKILL.md").write_text(
    -        "---\nname: feed-manage\ndescription: feed\n---\nbody\n",
    -        encoding="utf-8",
    -    )
    -    mgr = PluginManager(
    -        plugin_dirs=[builtin_root],
    -        installed_cache_root=cache_root,
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        manifest_path = mgr.sync_manifest(plugins_home=tmp_path / ".akashic-plugin")
    -        import tomllib
    -
    -        manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
    -        assert set(manifest["plugins"]) == {"feed@lab", "hello"}
    -        assert manifest["plugins"]["feed@lab"]["enabled"] is True
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_active_plugin_check_failure_is_recorded(tmp_path: Path) -> None:
    -    _write_v3_plugin(
    -        tmp_path / "plugins" / "broken_active",
    -        source=(
    -            "def is_active(services):\n"
    -            '    raise RuntimeError("active check failed")\n\n'
    -            "def apply(ctx, config):\n"
    -            "    return None\n"
    -        ),
    -    )
    -    manager = _make_manager(
    -        [tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -
    -    await manager.load_all()
    -
    -    assert manager.loaded_count == 0
    -    gate = manager.latest_gate("broken_active")
    -    assert gate is not None and gate.status == "failed"
    -    assert any("active check failed" in str(check.evidence) for check in gate.checks)
    -    await manager.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_manifest_disables_builtin_plugin(tmp_path: Path):
    -    plugin_root = tmp_path / "plugins"
    -    _write_v3_plugin(plugin_root / "configured", name="configured")
    -    from agent.plugins.manifest import write_plugin_manifest
    -
    -    write_plugin_manifest(
    -        {"configured": False},
    -        plugins_home=TEST_PLUGIN_HOME,
    -    )
    -    mgr = _make_manager(
    -        [plugin_root],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=TEST_PLUGIN_HOME / "cache",
    -    )
    -
    -    try:
    -        await mgr.load_all()
    -        assert mgr.loaded_count == 0
    -        assert mgr.active_plugins() == []
    -    finally:
    -        await mgr.terminate_all()
    -
    -
    -@pytest.mark.asyncio
    -async def test_core_runtime_stop_closes_session_manager(tmp_path: Path):
    -    from bootstrap.tools import CoreRuntime
    -    from session.manager import SessionManager
    -
    -    async def _noop() -> None:
    -        return None
    -
    -    session_manager = SessionManager(tmp_path)
    -    runtime = CoreRuntime(
    -        config=Config(system_prompt="s"),
    -        http_resources=SimpleNamespace(),  # type: ignore[arg-type]
    -        loop=SimpleNamespace(shutdown_compaction=_noop),  # type: ignore[arg-type]
    -        bus=SimpleNamespace(),  # type: ignore[arg-type]
    -        event_bus=SimpleNamespace(aclose=_noop),  # type: ignore[arg-type]
    -        tools=SimpleNamespace(get_tool=lambda _name: None),  # type: ignore[arg-type]
    -        push_tool=SimpleNamespace(),  # type: ignore[arg-type]
    -        session_manager=session_manager,
    -        presence=SimpleNamespace(),  # type: ignore[arg-type]
    -        plugin_manager=None,
    -    )
    -
    -    await runtime.stop()
    -
    -    assert session_manager._store._closed is True
    diff --git a/tests/test_plugin_mcp_generation_host.py b/tests/test_plugin_mcp_generation_host.py
    deleted file mode 100644
    index a0c51c92b..000000000
    --- a/tests/test_plugin_mcp_generation_host.py
    +++ /dev/null
    @@ -1,763 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import os
    -import socket
    -import subprocess
    -import sys
    -from pathlib import Path
    -
    -import pytest
    -
    -import agent.mcp.client as client_module
    -import agent.plugins.mcp_generation_host as mcp_host_module
    -from agent.plugin_composition import (
    -    MCP_SERVERS,
    -    CompositionRoot,
    -    EndpointEnv,
    -    McpServerDefinition,
    -    PluginRuntime,
    -)
    -from agent.plugin_composition.mcp_slots import (
    -    McpServerRegistry,
    -    PluginMcpServers,
    -    _freeze_plugin_mcp_servers,
    -)
    -from agent.plugins.mcp_generation_host import (
    -    McpGeneration,
    -    McpGenerationHost,
    -    McpMaterializedCommand,
    -    McpMode,
    -)
    -from utils.process_group import OwnedProcessGroup
    -
    -
    -def test_incident_error_text_has_no_blank_timeout_message() -> None:
    -    assert mcp_host_module._error_text(TimeoutError()) == "TimeoutError"
    -
    -
    -def _free_port() -> int:
    -    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
    -        listener.bind(("127.0.0.1", 0))
    -        return int(listener.getsockname()[1])
    -
    -
    -def _runtime(plugin_dir: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id=plugin_dir.name,
    -        generation_id="test-generation",
    -        plugin_dir=plugin_dir,
    -        data_dir=plugin_dir / "data",
    -        workspace=plugin_dir / "workspace",
    -        config=None,
    -    )
    -
    -
    -def _write_server(path: Path, *, mode: str = "normal") -> None:
    -    if mode == "always_exit":
    -        exit_code = "    if method == 'tools/list':\n        time.sleep(0.01); raise SystemExit(17)\n"
    -    elif mode == "first_exit":
    -        exit_code = (
    -            "    if method == 'tools/list' and epoch == 1:\n"
    -            "        time.sleep(0.01); raise SystemExit(17)\n"
    -        )
    -    elif mode == "hang_initialize":
    -        exit_code = "    if method == 'initialize':\n        time.sleep(30)\n"
    -    else:
    -        exit_code = ""
    -    catalog = (
    -        "            {'name': 'read_tool', 'description': 'changed', "
    -        "'inputSchema': {'type': 'object', 'properties': {'changed': {'type': 'boolean'}}}},\n"
    -        if mode == "catalog_drift"
    -        else "            {'name': 'read_tool', 'description': 'read', 'inputSchema': {'type': 'object'}},\n"
    -    )
    -    path.write_text(
    -        (
    -            "import json, os, sys, time\n"
    -            "from pathlib import Path\n"
    -            "counter_path = Path(os.environ.get('COUNTER', 'counter'))\n"
    -            "epoch = int(counter_path.read_text()) + 1 if counter_path.exists() else 1\n"
    -            "counter_path.write_text(str(epoch))\n"
    -            "print('server stderr epoch=' + str(epoch), file=sys.stderr, flush=True)\n"
    -            "for raw in sys.stdin:\n"
    -            "    msg = json.loads(raw); method = msg.get('method')\n"
    -            "    if method == 'initialize':\n"
    -            "        result = {'protocolVersion': '2025-11-25'}\n"
    -            "    elif method == 'tools/list':\n"
    -            "        result = {'tools': [\n"
    -            + catalog
    -            + "            {'name': 'write_tool', 'description': 'write', 'inputSchema': {'type': 'object'}},\n"
    -            + "        ]}\n"
    -            + "    elif method == 'tools/call':\n"
    -            + "        name = msg['params']['name']\n"
    -            + "        if name == 'write_tool':\n"
    -            + "            result = {'isError': True, 'content': [{'type': 'text', 'text': 'write denied'}]}\n"
    -            + "        else:\n"
    -            + "            result = {'content': [{'type': 'text', 'text': '|'.join((\n"
    -            + "                os.environ.get('ROLE', 'none'), os.environ.get('GEN', 'none'),\n"
    -            + "                os.environ.get('PORT', 'none'), str(epoch),\n"
    -            + "            ))}]}\n"
    -            + "    else:\n"
    -            + "        continue\n"
    -            + "    print(json.dumps({'jsonrpc': '2.0', 'id': msg['id'], 'result': result}), flush=True)\n"
    -            + exit_code
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -
    -async def _registry(
    -    tmp_path: Path,
    -    script: Path,
    -    *,
    -    required_tools: tuple[str, ...] = ("read_tool",),
    -    candidate_tools: tuple[str, ...] = ("read_tool",),
    -    candidate_env: dict[str, str] | None = None,
    -) -> tuple[CompositionRoot, McpServerRegistry]:
    -    plugin_dir = tmp_path / "calendar"
    -    plugin_dir.mkdir(exist_ok=True)
    -    (plugin_dir / script.name).write_text(script.read_text(encoding="utf-8"), encoding="utf-8")
    -    root = CompositionRoot("mcp-host-test")
    -    service = PluginMcpServers(root.instance_token)
    -    _ = await root.context.provide(MCP_SERVERS, service)
    -    projected_candidate_env = (
    -        {"ROLE": "candidate"} if candidate_env is None else candidate_env
    -    )
    -    definition = McpServerDefinition(
    -        name="calendar",
    -        command=("python", script.name),
    -        cwd=".",
    -        env={"DECLARED": "yes"},
    -        required_tools=required_tools,
    -        candidate_read_only_tools=candidate_tools,
    -        endpoint_env=(EndpointEnv("PORT", "calendar_api"),),
    -        candidate_env=projected_candidate_env,
    -    )
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(MCP_SERVERS).register(ctx, definition)
    -
    -    _ = await root.mount(
    -        apply,
    -        name="calendar",
    -        inject=(MCP_SERVERS,),
    -        runtime=_runtime(plugin_dir),
    -    )
    -    return root, _freeze_plugin_mcp_servers(service, root.instance_token)
    -
    -
    -def _command(script: Path, *, env: dict[str, str] | None = None) -> dict[str, McpMaterializedCommand]:
    -    return {
    -        "calendar": McpMaterializedCommand(
    -            command=(sys.executable, str(script)),
    -            cwd=str(script.parent),
    -            env={"COUNTER": str(script.parent / "counter"), **(env or {})},
    -        )
    -    }
    -
    -
    -async def _wait_until(predicate, *, timeout: float = 5.0) -> None:
    -    deadline = asyncio.get_running_loop().time() + timeout
    -    while not predicate():
    -        if asyncio.get_running_loop().time() >= deadline:
    -            raise AssertionError("condition did not become true before timeout")
    -        await asyncio.sleep(0.02)
    -
    -
    -def _generation_is_healthy(generation: McpGeneration) -> bool:
    -    try:
    -        generation.assert_healthy()
    -    except RuntimeError:
    -        return False
    -    return True
    -
    -
    -@pytest.mark.asyncio
    -async def test_candidate_filters_tools_and_projects_controlled_env(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        generation = await host.start_generation(
    -            "candidate-a",
    -            registry,
    -            _command(script, env={"GEN": "candidate-a"}),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        assert generation.state == "ready"
    -        assert generation.server("calendar").tool_names == ("read_tool",)
    -        assert len(generation.logs("calendar").stderr) <= 8
    -        result = await generation.route("calendar").call("read_tool", {})
    -        role, generation_name, port, _epoch = result.output.split("|")
    -        assert result.status == "success"
    -        assert role == "candidate"
    -        assert generation_name == "candidate-a"
    -        assert port.isdecimal()
    -        with pytest.raises(PermissionError, match="allowlist"):
    -            await generation.route("calendar").call("write_tool", {})
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_duplicate_generation_is_rejected_before_second_process(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        endpoint_ports = {"calendar_api": _free_port()}
    -        await host.start_generation(
    -            "same",
    -            registry,
    -            _command(script),
    -            endpoint_ports=endpoint_ports,
    -        )
    -        with pytest.raises(RuntimeError, match="already exists"):
    -            await host.start_generation(
    -                "same",
    -                registry,
    -                _command(script),
    -                endpoint_ports=endpoint_ports,
    -            )
    -        assert (script.parent / "counter").read_text(encoding="utf-8") == "1"
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_exposes_all_tools_and_does_not_apply_candidate_env(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        generation = await host.start_generation(
    -            "formal-a",
    -            registry,
    -            _command(script, env={"GEN": "formal-a"}),
    -            mode="formal",
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        assert generation.server("calendar").tool_names == ("read_tool", "write_tool")
    -        result = await generation.route("calendar").call("write_tool", {})
    -        assert result.status == "tool_error"
    -        read_result = await generation.route("calendar").call("read_tool", {})
    -        assert read_result.output.startswith("none|formal-a|")
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_child_env_scrubs_ambient_candidate_value(monkeypatch, tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    monkeypatch.setenv("ROLE", "candidate")
    -    host = McpGenerationHost()
    -    try:
    -        formal = await host.start_generation(
    -            "formal-ambient-env",
    -            registry,
    -            _command(script),
    -            mode="formal",
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        formal_result = await formal.route("calendar").call("read_tool", {})
    -        assert formal_result.output.split("|", 1)[0] == "none"
    -        await host.stop_generation("formal-ambient-env")
    -
    -        candidate = await host.start_generation(
    -            "candidate-ambient-env",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        candidate_result = await candidate.route("calendar").call("read_tool", {})
    -        assert candidate_result.output.split("|", 1)[0] == "candidate"
    -        await host.stop_generation("candidate-ambient-env")
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -def test_host_reload_does_not_mutate_shared_process_environment_builder() -> None:
    -    probe = subprocess.run(
    -        [
    -            sys.executable,
    -            "-c",
    -            (
    -                "import importlib\n"
    -                "import agent.mcp.client as client\n"
    -                "import agent.plugins.mcp_generation_host as host\n"
    -                "original = client.owned_process_env\n"
    -                "importlib.reload(host)\n"
    -                "assert client.owned_process_env is original\n"
    -                "assert isinstance(client.owned_process_env({}), dict)\n"
    -            ),
    -        ],
    -        cwd=Path(__file__).parents[1],
    -        check=False,
    -        capture_output=True,
    -        text=True,
    -    )
    -
    -    assert probe.returncode == 0, probe.stderr
    -
    -
    -@pytest.mark.asyncio
    -async def test_formal_rejects_catalog_drift_from_candidate_identity(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        candidate = await host.start_generation(
    -            "candidate-catalog",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        expected_digest = candidate.catalog_digest("calendar")
    -        _write_server(script, mode="catalog_drift")
    -        with pytest.raises(RuntimeError, match="catalog drift"):
    -            await host.start_generation(
    -                "formal-catalog-drift",
    -                registry,
    -                _command(script),
    -                mode="formal",
    -                endpoint_ports={"calendar_api": _free_port()},
    -                expected_catalog_digests={"calendar": expected_digest},
    -            )
    -        assert host.get("formal-catalog-drift") is None
    -        assert host.tombstone("formal-catalog-drift") is None
    -        assert candidate.catalog_digest("calendar") == expected_digest
    -        await host.stop_generation("candidate-catalog")
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_materialized_candidate_env_is_rejected_for_candidate_and_formal(
    -    tmp_path: Path,
    -) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        cases: tuple[tuple[str, McpMode], ...] = (
    -            ("candidate-base-env", "candidate"),
    -            ("formal-base-env", "formal"),
    -        )
    -        for generation_id, mode in cases:
    -            with pytest.raises(ValueError, match="candidate-only"):
    -                await host.start_generation(
    -                    generation_id,
    -                    registry,
    -                    _command(script, env={"ROLE": "candidate"}),
    -                    mode=mode,
    -                    endpoint_ports={"calendar_api": _free_port()},
    -                )
    -            assert host.get(generation_id) is None
    -        assert not (script.parent / "counter").exists()
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_declaration_rejects_overlapping_candidate_env(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(
    -        tmp_path,
    -        script,
    -        candidate_env={"DECLARED": "candidate"},
    -    )
    -    host = McpGenerationHost()
    -    try:
    -        with pytest.raises(ValueError, match="不得重叠"):
    -            await host.start_generation(
    -                "formal-overlap",
    -                registry,
    -                _command(script),
    -                mode="formal",
    -                endpoint_ports={"calendar_api": _free_port()},
    -            )
    -        assert host.get("formal-overlap") is None
    -        assert not (script.parent / "counter").exists()
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_materialized_argv0_must_be_pinned_absolute_executable(
    -    tmp_path: Path,
    -) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        with pytest.raises(ValueError, match=r"argv\[0\].*absolute executable"):
    -            await host.start_generation(
    -                "candidate-path-hostile",
    -                registry,
    -                {
    -                    "calendar": McpMaterializedCommand(
    -                        command=("python", str(script)),
    -                        cwd=str(script.parent),
    -                        env={"COUNTER": str(script.parent / "counter")},
    -                    )
    -                },
    -                endpoint_ports={"calendar_api": _free_port()},
    -            )
    -        assert host.get("candidate-path-hostile") is None
    -        assert not (script.parent / "counter").exists()
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_readiness_rejects_missing_required_tool_and_cleans_process(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(
    -        tmp_path,
    -        script,
    -        required_tools=("missing",),
    -        candidate_tools=(),
    -    )
    -    host = McpGenerationHost()
    -    try:
    -        with pytest.raises(RuntimeError, match="required tool 缺失"):
    -            await host.start_generation(
    -                "candidate-missing",
    -                registry,
    -                _command(script),
    -                endpoint_ports={"calendar_api": _free_port()},
    -            )
    -        assert host.get("candidate-missing") is None
    -        assert host.tombstone("candidate-missing") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_start_cancellation_drains_client_and_restores_cancelled_error(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script, mode="hang_initialize")
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost(readiness_timeout_seconds=30)
    -    task = asyncio.create_task(
    -        host.start_generation(
    -            "candidate-cancel",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -    )
    -    try:
    -        await asyncio.sleep(0.1)
    -        task.cancel()
    -        with pytest.raises(asyncio.CancelledError):
    -            await task
    -        assert host.get("candidate-cancel") is None
    -        assert host.tombstone("candidate-cancel") is None
    -        assert not [
    -            current
    -            for current in asyncio.all_tasks()
    -            if current is not asyncio.current_task()
    -            and current.get_name().startswith("mcp_generation_")
    -        ]
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_start_health_bridge_cancellation_drains_client(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -
    -    def cancel_start(
    -        generation_id: str,
    -        server_name: str,
    -        healthy: bool,
    -        reason: str,
    -    ) -> None:
    -        if reason == "starting":
    -            raise asyncio.CancelledError
    -
    -    host = McpGenerationHost(on_health=cancel_start)
    -    try:
    -        with pytest.raises(asyncio.CancelledError):
    -            await host.start_generation(
    -                "candidate-health-cancel",
    -                registry,
    -                _command(script),
    -                endpoint_ports={"calendar_api": _free_port()},
    -            )
    -        assert host.get("candidate-health-cancel") is None
    -        assert host.tombstone("candidate-health-cancel") is None
    -        assert not [
    -            current
    -            for current in asyncio.all_tasks()
    -            if current is not asyncio.current_task()
    -            and current.get_name().startswith("mcp_generation_")
    -        ]
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_client_epoch_recovery_is_fenced_and_bounded(tmp_path: Path, monkeypatch) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script, mode="first_exit")
    -    monkeypatch.setattr(client_module, "_RECOVERY_DELAYS", (0.01, 0.01, 0.01))
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        generation = await host.start_generation(
    -            "candidate-recover",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        initial_epoch = generation.server("calendar").epoch
    -        await _wait_until(lambda: generation.server("calendar").epoch > initial_epoch)
    -        await _wait_until(lambda: _generation_is_healthy(generation))
    -        generation.assert_healthy()
    -        result = await generation.route("calendar").call("read_tool", {})
    -        assert result.status == "success"
    -        assert result.output.endswith("|2")
    -        await host.stop_generation("candidate-recover")
    -        assert host.get("candidate-recover") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_recovery_health_bridge_cancellation_retains_degraded_tombstone(
    -    tmp_path: Path,
    -    monkeypatch,
    -) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script, mode="first_exit")
    -    monkeypatch.setattr(client_module, "_RECOVERY_DELAYS", (0.01, 0.01, 0.01))
    -    root, registry = await _registry(tmp_path, script)
    -
    -    def cancel_epoch_incident(
    -        generation_id: str,
    -        server_name: str,
    -        kind: str,
    -        message: str,
    -    ) -> None:
    -        if kind == "process_epoch":
    -            raise asyncio.CancelledError
    -
    -    host = McpGenerationHost(on_incident=cancel_epoch_incident)
    -    try:
    -        generation = await host.start_generation(
    -            "candidate-recovery-health-cancel",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        await _wait_until(
    -            lambda: host.tombstone("candidate-recovery-health-cancel") is not None,
    -            timeout=5,
    -        )
    -        assert generation.state == "degraded"
    -        tombstone = host.tombstone("candidate-recovery-health-cancel")
    -        assert tombstone is not None
    -        assert tombstone.state == "degraded"
    -        assert tombstone.action == "retry_runtime_recovery"
    -        await host.retry_runtime_recovery("candidate-recovery-health-cancel")
    -        assert host.get("candidate-recovery-health-cancel") is None
    -        assert host.tombstone("candidate-recovery-health-cancel") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_cleanup_failure_retains_tombstone_until_retry(tmp_path: Path, monkeypatch) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    await host.start_generation(
    -        "candidate-cleanup",
    -        registry,
    -        _command(script),
    -        endpoint_ports={"calendar_api": _free_port()},
    -    )
    -    original_terminate = OwnedProcessGroup.terminate
    -    calls = 0
    -
    -    async def fail_once(self: OwnedProcessGroup, *, timeout_s: float) -> None:
    -        nonlocal calls
    -        calls += 1
    -        if calls == 1:
    -            raise RuntimeError("injected terminate failure")
    -        await original_terminate(self, timeout_s=timeout_s)
    -
    -    monkeypatch.setattr(OwnedProcessGroup, "terminate", fail_once)
    -    try:
    -        with pytest.raises(RuntimeError, match="cleanup failed"):
    -            await host.stop_generation("candidate-cleanup")
    -        tombstone = host.tombstone("candidate-cleanup")
    -        assert tombstone is not None
    -        assert tombstone.action == "retry_generation_cleanup"
    -        assert host.get("candidate-cleanup") is not None
    -        await host.retry_generation_cleanup("candidate-cleanup")
    -        assert host.tombstone("candidate-cleanup") is None
    -        assert host.get("candidate-cleanup") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_stopped_health_failure_is_logged_not_cleanup_failure(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -
    -    def fail_stopped(
    -        generation_id: str,
    -        server_name: str,
    -        healthy: bool,
    -        reason: str,
    -    ) -> None:
    -        if reason == "stopped":
    -            raise RuntimeError("health sink disposed")
    -
    -    host = McpGenerationHost(on_health=fail_stopped)
    -    try:
    -        await host.start_generation(
    -            "candidate-observation-failure",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        await host.stop_generation("candidate-observation-failure")
    -        assert host.get("candidate-observation-failure") is None
    -        assert host.tombstone("candidate-observation-failure") is None
    -        assert any(
    -            "health sink disposed" in record.getMessage()
    -            for record in caplog.records
    -        )
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_degraded_recovery_tombstone_is_actionable(tmp_path: Path, monkeypatch) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script, mode="always_exit")
    -    monkeypatch.setattr(client_module, "_RECOVERY_DELAYS", (0.01, 0.01, 0.01))
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        _ = await host.start_generation(
    -            "candidate-degraded",
    -            registry,
    -            _command(script),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        await _wait_until(
    -            lambda: host.tombstone("candidate-degraded") is not None,
    -            timeout=5,
    -        )
    -        tombstone = host.tombstone("candidate-degraded")
    -        assert tombstone is not None
    -        assert tombstone.state == "degraded"
    -        assert tombstone.action == "retry_runtime_recovery"
    -        await host.retry_runtime_recovery("candidate-degraded")
    -        assert host.get("candidate-degraded") is None
    -        assert host.tombstone("candidate-degraded") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_same_server_name_isolated_across_generations(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -    host = McpGenerationHost()
    -    try:
    -        first = await host.start_generation(
    -            "generation-a",
    -            registry,
    -            _command(script, env={"GEN": "a"}),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        second = await host.start_generation(
    -            "generation-b",
    -            registry,
    -            _command(script, env={"GEN": "b"}),
    -            endpoint_ports={"calendar_api": _free_port()},
    -        )
    -        assert (await first.route("calendar").call("read_tool", {})).output.startswith(
    -            "candidate|a|"
    -        )
    -        assert (await second.route("calendar").call("read_tool", {})).output.startswith(
    -            "candidate|b|"
    -        )
    -        await host.stop_generation("generation-a")
    -        with pytest.raises(RuntimeError, match="stale|不可调用"):
    -            await first.route("calendar").call("read_tool", {})
    -        with pytest.raises(RuntimeError, match="stale|不可用"):
    -            _ = first.state
    -        with pytest.raises(RuntimeError, match="stale|不可用"):
    -            first.assert_healthy()
    -        assert (await second.route("calendar").call("read_tool", {})).status == "success"
    -    finally:
    -        await host.close()
    -        await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_health_bridge_failure_prevents_ready_publication(tmp_path: Path) -> None:
    -    script = tmp_path / "server.py"
    -    _write_server(script)
    -    root, registry = await _registry(tmp_path, script)
    -
    -    def fail_ready(generation_id: str, server_name: str, healthy: bool, reason: str) -> None:
    -        if reason == "ready":
    -            raise RuntimeError("health bridge failed")
    -
    -    host = McpGenerationHost(on_health=fail_ready)
    -    try:
    -        with pytest.raises(RuntimeError, match="health bridge failed"):
    -            await host.start_generation(
    -                "candidate-health-failure",
    -                registry,
    -                _command(script),
    -                endpoint_ports={"calendar_api": _free_port()},
    -            )
    -        assert host.get("candidate-health-failure") is None
    -        assert host.tombstone("candidate-health-failure") is None
    -    finally:
    -        await host.close()
    -        await root.dispose()
    diff --git a/tests/test_plugin_mobile_ui.py b/tests/test_plugin_mobile_ui.py
    deleted file mode 100644
    index 617840afe..000000000
    --- a/tests/test_plugin_mobile_ui.py
    +++ /dev/null
    @@ -1,475 +0,0 @@
    -from __future__ import annotations
    -
    -from collections.abc import Iterator, Mapping
    -import asyncio
    -from concurrent.futures import ThreadPoolExecutor
    -import hashlib
    -import logging
    -import threading
    -from types import MappingProxyType, SimpleNamespace
    -from typing import Any, cast
    -
    -import pytest
    -
    -import agent.plugins.mobile_ui as mobile_ui_module
    -from agent.plugin_composition import (
    -    MobileUiBinding,
    -    MobileUiDescriptor,
    -    MobileUiRegistry,
    -)
    -from agent.plugins.generation import MobileUiAsset
    -from agent.plugins.generation import PluginGeneration
    -from agent.plugins.mobile_ui import (
    -    MobileUiPluginUnavailable,
    -    MobileUiQueryTimeout,
    -    MobileUiRpcExecutionError,
    -    MobileUiStaleRevision,
    -    PluginMobileUiProvider,
    -)
    -from agent.plugins.snapshot import RuntimeSnapshot
    -
    -
    -class _MobilePlugin:
    -    def __init__(self, *, available: bool = True) -> None:
    -        self.available = available
    -        self.query_handler: Any = self._default_query
    -
    -    def mobile_ui_available(self) -> bool:
    -        return self.available
    -
    -    def mobile_ui_query(
    -        self,
    -        method: str,
    -        payload: dict[str, object],
    -        *,
    -        session_id: str | None,
    -        turn_id: str | None,
    -    ) -> dict[str, object]:
    -        return self.query_handler(
    -            method,
    -            payload,
    -            session_id=session_id,
    -            turn_id=turn_id,
    -        )
    -
    -    @staticmethod
    -    def _default_query(
    -        method: str,
    -        payload: dict[str, object],
    -        *,
    -        session_id: str | None,
    -        turn_id: str | None,
    -    ) -> dict[str, object]:
    -        return {
    -            "method": method,
    -            "payload": payload,
    -            "session_id": session_id,
    -            "turn_id": turn_id,
    -        }
    -
    -
    -class _Lease:
    -    def __init__(self, store: "_Store", snapshot: object) -> None:
    -        self._store = store
    -        self.snapshot = snapshot
    -
    -    async def __aenter__(self):
    -        self._store.entered += 1
    -        return self.snapshot
    -
    -    async def __aexit__(self, *args: object) -> None:
    -        self._store.exited += 1
    -
    -
    -class _Store:
    -    def __init__(self, snapshot: object) -> None:
    -        self.snapshot = snapshot
    -        self.entered = 0
    -        self.exited = 0
    -
    -    async def acquire(self) -> _Lease:
    -        return _Lease(self, self.snapshot)
    -
    -
    -class _ExplodingMapping(Mapping[str, object]):
    -    def __getitem__(self, key: str) -> object:
    -        raise KeyError(key)
    -
    -    def __iter__(self) -> Iterator[str]:
    -        raise RuntimeError("mapping iteration failed")
    -
    -    def __len__(self) -> int:
    -        return 1
    -
    -
    -def _provider(*, available: bool = True) -> PluginMobileUiProvider:
    -    module = "export default 1;"
    -    stylesheet = ":host { color: red; }"
    -    asset = MobileUiAsset(
    -        module=module,
    -        module_sha256=hashlib.sha256(module.encode()).hexdigest(),
    -        module_bytes=len(module.encode()),
    -        stylesheet=stylesheet,
    -        stylesheet_sha256=hashlib.sha256(stylesheet.encode()).hexdigest(),
    -        stylesheet_bytes=len(stylesheet.encode()),
    -        navigation_label="Sample",
    -        navigation_description="Sample dashboard",
    -        slots=("turn.after_answer",),
    -    )
    -    plugin = _MobilePlugin(available=available)
    -    binding = MobileUiBinding(
    -        descriptor=MobileUiDescriptor(
    -            owner="sample@github",
    -            module_sha256=asset.module_sha256,
    -            module_bytes=asset.module_bytes,
    -            stylesheet_sha256=asset.stylesheet_sha256,
    -            stylesheet_bytes=asset.stylesheet_bytes,
    -            navigation_label=asset.navigation_label,
    -            navigation_description=asset.navigation_description,
    -            slots=asset.slots,
    -        ),
    -        asset=asset,
    -        query=plugin.mobile_ui_query,
    -        available=plugin.mobile_ui_available,
    -    )
    -    generation = SimpleNamespace(
    -        plugin_id="sample@github",
    -        generation_id="generation-sample",
    -        source_revision="revision-1",
    -        instance=plugin,
    -        contributions=SimpleNamespace(),
    -    )
    -    snapshot = RuntimeSnapshot(
    -        snapshot_id="snapshot-1",
    -        generations=MappingProxyType(
    -            {"sample@github": cast(PluginGeneration, generation)}
    -        ),
    -        skill_catalog_generation_id=None,
    -        mobile_ui_registry=MobileUiRegistry({"sample@github": binding}),
    -        composition_active_plugin_ids=frozenset({"sample@github"}),
    -    )
    -    manager = SimpleNamespace(current_snapshot=snapshot, snapshot_store=_Store(snapshot))
    -    return PluginMobileUiProvider(cast(Any, manager))
    -
    -
    -def _diagnostic_fields(record: logging.LogRecord) -> dict[str, object]:
    -    return cast(dict[str, object], getattr(record, "akashic_fields"))
    -
    -
    -def test_mobile_ui_catalog_separates_metadata_from_content_addressed_assets() -> None:
    -    provider = _provider()
    -
    -    catalog = provider.catalog()
    -    item = cast(list[dict[str, object]], catalog["items"])[0]
    -    module = provider.asset(
    -        "sample@github",
    -        "revision-1",
    -        "module",
    -        cast(str, item["module_sha256"]),
    -    )
    -    stylesheet = provider.asset(
    -        "sample@github",
    -        "revision-1",
    -        "stylesheet",
    -        cast(str, item["stylesheet_sha256"]),
    -    )
    -
    -    assert isinstance(catalog["catalog_revision"], str)
    -    assert "content" not in item
    -    assert item["navigation"] == {
    -        "label": "Sample",
    -        "description": "Sample dashboard",
    -    }
    -    assert item["slots"] == ["turn.after_answer"]
    -    assert module["content"] == "export default 1;"
    -    assert stylesheet["content"] == ":host { color: red; }"
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_requires_exact_registry_binding() -> None:
    -    provider = _provider()
    -    manager = cast(Any, provider)._manager
    -    manager.current_snapshot.mobile_ui_registry = None
    -
    -    assert provider.catalog()["items"] == []
    -    with pytest.raises(MobileUiPluginUnavailable):
    -        provider.asset(
    -            "sample@github",
    -            "revision-1",
    -            "module",
    -            hashlib.sha256(b"export default 1;").hexdigest(),
    -        )
    -    with pytest.raises(MobileUiPluginUnavailable):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "recall.current",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_unavailable_capability_is_hidden_and_rejected() -> None:
    -    provider = _provider(available=False)
    -
    -    assert provider.catalog()["items"] == []
    -    with pytest.raises(MobileUiPluginUnavailable):
    -        provider.asset("sample@github", "revision-1", "module", "0" * 64)
    -    with pytest.raises(MobileUiPluginUnavailable):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "recall.current",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_query_receives_revision_and_turn_context(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    provider = _provider()
    -
    -    result = await provider.query(
    -        "sample@github",
    -        "revision-1",
    -        "recall.current",
    -        {"limit": 4},
    -        session_id="mobile:test",
    -        turn_id="turn-1",
    -    )
    -
    -    assert result == {
    -        "method": "recall.current",
    -        "payload": {"limit": 4},
    -        "session_id": "mobile:test",
    -        "turn_id": "turn-1",
    -    }
    -    terminal = next(
    -        _diagnostic_fields(record)
    -        for record in caplog.records
    -        if _diagnostic_fields(record).get("event") == "plugin.operation.done"
    -        and _diagnostic_fields(record).get("operation") == "mobile_ui.query"
    -    )
    -    assert terminal["operation"] == "mobile_ui.query"
    -    assert terminal["generation_id"] == "generation-sample"
    -    assert terminal["session_id"] == "mobile:test"
    -    assert terminal["turn_id"] == "turn-1"
    -    assert "plugin_entrypoint" not in terminal
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_sync_query_never_blocks_event_loop() -> None:
    -    provider = _provider()
    -    entered = threading.Event()
    -    release = threading.Event()
    -
    -    def block(*args: object, **kwargs: object) -> dict[str, object]:
    -        entered.set()
    -        release.wait(timeout=1)
    -        return {"status": "ready"}
    -
    -    generation = cast(Any, provider)._manager.current_snapshot.generations[
    -        "sample@github"
    -    ]
    -    generation.instance.query_handler = block
    -    query = asyncio.create_task(
    -        provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "health.snapshot",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -    )
    -
    -    assert await asyncio.to_thread(entered.wait, 1)
    -    heartbeat = asyncio.create_task(asyncio.sleep(0))
    -    await asyncio.wait_for(heartbeat, timeout=0.1)
    -    assert not query.done()
    -    release.set()
    -    assert await query == {"status": "ready"}
    -
    -
    -def test_mobile_ui_rejects_inactive_or_stale_plugin_assets() -> None:
    -    provider = _provider()
    -    item = cast(list[dict[str, object]], provider.catalog()["items"])[0]
    -
    -    with pytest.raises(MobileUiStaleRevision, match="sample"):
    -        provider.asset(
    -            "sample@github",
    -            "old-revision",
    -            "module",
    -            cast(str, item["module_sha256"]),
    -        )
    -
    -    cast(Any, provider)._manager.current_snapshot.composition_active_plugin_ids = (
    -        frozenset()
    -    )
    -    with pytest.raises(MobileUiPluginUnavailable, match="sample"):
    -        provider.asset(
    -            "sample@github",
    -            "revision-1",
    -            "module",
    -            cast(str, item["module_sha256"]),
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_timeout_keeps_snapshot_lease_until_worker_exits(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    provider = _provider()
    -    blocker = threading.Event()
    -
    -    def block(*args: object, **kwargs: object) -> dict[str, object]:
    -        blocker.wait(timeout=1)
    -        return {}
    -
    -    generation = cast(Any, provider)._manager.current_snapshot.generations[
    -        "sample@github"
    -    ]
    -    generation.instance.query_handler = block
    -    store = cast(Any, provider)._manager.snapshot_store
    -    monkeypatch.setattr(mobile_ui_module, "MOBILE_UI_QUERY_TIMEOUT_SECONDS", 0.01)
    -
    -    with pytest.raises(MobileUiQueryTimeout):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "recall.current",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -
    -    assert store.entered == 1
    -    assert store.exited == 0
    -    blocker.set()
    -    for _ in range(20):
    -        if store.exited == 1:
    -            break
    -        await asyncio.sleep(0.01)
    -    assert store.exited == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_query_rejects_beyond_bounded_worker_queue(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    provider = _provider()
    -    blocker = threading.Event()
    -    entered = threading.Event()
    -    monkeypatch.setattr(mobile_ui_module, "MOBILE_UI_QUERY_WORKERS", 1)
    -    monkeypatch.setattr(mobile_ui_module, "MOBILE_UI_QUERY_QUEUE_LIMIT", 0)
    -    cast(Any, provider)._executor.shutdown(wait=True)
    -    cast(Any, provider)._executor = ThreadPoolExecutor(
    -        max_workers=1,
    -        thread_name_prefix="mobile-plugin-ui-test",
    -    )
    -
    -    def block(*args: object, **kwargs: object) -> dict[str, object]:
    -        entered.set()
    -        blocker.wait(timeout=1)
    -        return {}
    -
    -    cast(Any, provider)._manager.current_snapshot.generations[
    -        "sample@github"
    -    ].instance.query_handler = block
    -    running = asyncio.create_task(
    -        provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "health.snapshot",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -    )
    -    assert await asyncio.to_thread(entered.wait, 1)
    -    with pytest.raises(mobile_ui_module.MobileUiQueryOverloaded):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "health.snapshot",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-2",
    -        )
    -    blocker.set()
    -    assert await running == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_mobile_ui_rpc_failure_isolated_from_transport() -> None:
    -    provider = _provider()
    -
    -    def fails(*args: object, **kwargs: object) -> dict[str, object]:
    -        raise RuntimeError("plugin bug detail")
    -
    -    cast(Any, provider)._manager.current_snapshot.generations[
    -        "sample@github"
    -    ].instance.query_handler = fails
    -
    -    with pytest.raises(MobileUiRpcExecutionError, match="sample@github.recall.current"):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "recall.current",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    "invalid_result",
    -    (
    -        [],
    -        {1: "value"},
    -        {"value": {1: "nested"}},
    -        {"value": object()},
    -        {"value": "x" * (193 * 1024)},
    -        _ExplodingMapping(),
    -    ),
    -)
    -async def test_mobile_ui_rpc_invalid_result_isolated_from_transport(
    -    invalid_result: object,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    caplog.set_level(logging.INFO, logger="akashic.plugin.diagnostics")
    -    provider = _provider()
    -
    -    def returns_invalid(*args: object, **kwargs: object) -> object:
    -        return invalid_result
    -
    -    cast(Any, provider)._manager.current_snapshot.generations[
    -        "sample@github"
    -    ].instance.query_handler = returns_invalid
    -
    -    with pytest.raises(MobileUiRpcExecutionError, match="sample@github.recall.current"):
    -        await provider.query(
    -            "sample@github",
    -            "revision-1",
    -            "recall.current",
    -            {},
    -            session_id="mobile:test",
    -            turn_id="turn-1",
    -        )
    -    terminal = next(
    -        _diagnostic_fields(record)
    -        for record in caplog.records
    -        if _diagnostic_fields(record).get("event")
    -        == "plugin.operation.error"
    -        and _diagnostic_fields(record).get("operation") == "mobile_ui.query"
    -    )
    -    assert terminal["operation"] == "mobile_ui.query"
    -    assert terminal["session_id"] == "mobile:test"
    -    assert terminal["turn_id"] == "turn-1"
    diff --git a/tests/test_plugin_reload_journal.py b/tests/test_plugin_reload_journal.py
    deleted file mode 100644
    index 7c136563c..000000000
    --- a/tests/test_plugin_reload_journal.py
    +++ /dev/null
    @@ -1,443 +0,0 @@
    -import sqlite3
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugins.reload_journal import ReloadJournal
    -
    -
    -def test_reload_journal_records_durable_transaction_phases(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="weather",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="weather:source-v2:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -        base_artifact_pointer=".artifacts/v1",
    -        candidate_artifact_pointer=".artifacts/v2",
    -    )
    -    journal.mark_runtime_owner(tx_id, "boot-v1")
    -
    -    journal.advance(
    -        tx_id,
    -        "prepared",
    -        candidate_snapshot_id="snapshot-v2",
    -    )
    -    journal.advance(tx_id, "validating")
    -    journal.advance(tx_id, "commit_started")
    -    journal.advance(tx_id, "committed")
    -    journal.advance(tx_id, "draining")
    -
    -    record = journal.get(tx_id)
    -    assert record.phase == "draining"
    -    assert record.plugin_id == "weather"
    -    assert record.base_snapshot_id == "snapshot-v1"
    -    assert record.candidate_snapshot_id == "snapshot-v2"
    -    assert record.source_revision == "source-v2"
    -    assert record.runtime_owner_boot_id == "boot-v1"
    -    assert record.base_artifact_pointer == ".artifacts/v1"
    -    assert record.candidate_artifact_pointer == ".artifacts/v2"
    -    assert [event.phase for event in journal.events(tx_id)] == [
    -        "preparing",
    -        "preparing",
    -        "prepared",
    -        "validating",
    -        "commit_started",
    -        "committed",
    -        "draining",
    -    ]
    -
    -    reopened = ReloadJournal(tmp_path / "workspace")
    -    assert reopened.get(tx_id) == record
    -
    -    with pytest.raises(RuntimeError, match="不可覆盖"):
    -        reopened.mark_runtime_owner(tx_id, "boot-v2")
    -
    -
    -def test_reload_journal_rejects_invalid_phase_transition(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="weather",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="weather:source-v2:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -
    -    with pytest.raises(RuntimeError, match="ReloadTransaction 状态跳转无效"):
    -        journal.advance(tx_id, "committed")
    -
    -
    -def test_reload_journal_recovers_discarding_candidate(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="weather",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="weather:source-v2:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -    journal.advance(tx_id, "prepared", candidate_snapshot_id="snapshot-v2")
    -    journal.advance(tx_id, "validating")
    -    journal.advance(tx_id, "commit_started")
    -    journal.advance(tx_id, "latest_ready")
    -    journal.advance(tx_id, "discarding")
    -
    -    action = journal.pending_recovery()[0]
    -
    -    assert action.phase == "discarding"
    -    assert action.action == "discard_candidate"
    -    journal.finish_recovery(action)
    -    assert journal.get(tx_id).phase == "aborted"
    -
    -
    -def test_reload_journal_builds_crash_recovery_plan(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    prepared = journal.begin(
    -        plugin_id="weather",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="weather:source-v2:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -    journal.advance(prepared, "prepared")
    -    committed = journal.begin(
    -        plugin_id="calendar",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="calendar:source-v3:3",
    -        source_revision="source-v3",
    -        config_revision="config-v3",
    -    )
    -    journal.advance(
    -        committed,
    -        "prepared",
    -        candidate_snapshot_id="snapshot-v3",
    -    )
    -    journal.advance(committed, "validating")
    -    journal.advance(committed, "commit_started")
    -    latest = journal.begin(
    -        plugin_id="feed",
    -        base_snapshot_id="snapshot-v1",
    -        generation_id="feed:source-v4:4",
    -        source_revision="source-v4",
    -        config_revision="config-v4",
    -    )
    -    journal.advance(latest, "prepared", candidate_snapshot_id="snapshot-v4")
    -    journal.advance(latest, "validating")
    -    journal.advance(latest, "commit_started")
    -    journal.advance(latest, "latest_ready")
    -
    -    actions = journal.pending_recovery()
    -
    -    assert (actions[0].tx_id, actions[0].phase, actions[0].action) == (
    -        committed,
    -        "commit_started",
    -        "restore_committed",
    -    )
    -    assert {
    -        (action.tx_id, action.phase, action.action) for action in actions[1:]
    -    } == {
    -        (latest, "latest_ready", "discard_candidate"),
    -        (prepared, "prepared", "discard_candidate"),
    -    }
    -    for action in actions:
    -        journal.finish_recovery(action)
    -    assert journal.get(committed).phase == "recovered"
    -    assert journal.get(latest).phase == "aborted"
    -    assert journal.get(prepared).phase == "aborted"
    -    assert journal.pending_recovery() == ()
    -
    -
    -def test_reload_journal_retains_cleanup_failure_evidence_across_restart(
    -    tmp_path: Path,
    -) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="calendar",
    -        base_snapshot_id="stable-snapshot-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -    journal.advance(tx_id, "prepared", candidate_snapshot_id="candidate-snapshot-v2")
    -    journal.advance(tx_id, "validating")
    -    journal.advance(tx_id, "commit_started")
    -    journal.advance(tx_id, "latest_ready")
    -    journal.advance(tx_id, "promoting")
    -    journal.advance(
    -        tx_id,
    -        "cleanup_failed",
    -        resource="calendar_api@candidate:2",
    -        formal_effects=("old_endpoint_stopped", "new_endpoint_started"),
    -        error="terminate failed",
    -        recovery_target="base",
    -    )
    -
    -    reopened = ReloadJournal(tmp_path / "workspace")
    -    record = reopened.get(tx_id)
    -    assert record.phase == "cleanup_failed"
    -    assert record.old_snapshot_id == "stable-snapshot-v1"
    -    assert record.new_snapshot_id == "candidate-snapshot-v2"
    -    assert record.old_generation_id == "calendar:stable:1"
    -    assert record.attempt_generation_id == "calendar:candidate:2"
    -    assert record.formal_effects == (
    -        "old_endpoint_stopped",
    -        "new_endpoint_started",
    -    )
    -    assert record.resource == "calendar_api@candidate:2"
    -    assert record.error == "terminate failed"
    -    assert record.attempt_count == 1
    -    failure_event = reopened.events(tx_id)[-1]
    -    assert failure_event.details["old_snapshot_id"] == "stable-snapshot-v1"
    -    assert failure_event.details["new_snapshot_id"] == "candidate-snapshot-v2"
    -    assert failure_event.details["attempt_generation_id"] == "calendar:candidate:2"
    -    assert failure_event.details["resource"] == "calendar_api@candidate:2"
    -    assert failure_event.details["error"] == "terminate failed"
    -    assert failure_event.details["attempt_count"] == 1
    -
    -    action = reopened.pending_recovery()[0]
    -    assert action.action == "retry_generation_cleanup"
    -    assert action.error == "terminate failed"
    -    assert action.attempt_count == 1
    -    with pytest.raises(RuntimeError, match="状态跳转无效"):
    -        reopened.advance(tx_id, "complete")
    -
    -    reopened.advance(
    -        tx_id,
    -        "cleanup_failed",
    -        resource="calendar_api@candidate:2",
    -        error="retry terminate failed",
    -    )
    -    assert reopened.get(tx_id).attempt_count == 2
    -    with pytest.raises(RuntimeError, match="recovery action 已失效"):
    -        reopened.finish_recovery(action)
    -    retry_action = reopened.pending_recovery()[0]
    -    with pytest.raises(RuntimeError, match="缺少 Host retry receipt"):
    -        reopened.finish_recovery(retry_action)
    -    with pytest.raises(RuntimeError, match="状态跳转无效"):
    -        reopened.advance(tx_id, "aborted")
    -    reopened.finish_recovery(
    -        retry_action,
    -        retry_receipt="managed-process-host:calendar:candidate:2:cleanup-complete",
    -    )
    -    recovered = reopened.get(tx_id)
    -    assert recovered.phase == "aborted"
    -    assert recovered.error == "retry terminate failed"
    -    assert reopened.pending_recovery() == ()
    -
    -
    -def test_reload_journal_degraded_runtime_recovery_is_explicit(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="calendar",
    -        base_snapshot_id="stable-snapshot-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -    journal.advance(tx_id, "prepared")
    -    journal.advance(tx_id, "validating")
    -    journal.advance(tx_id, "commit_started")
    -    journal.advance(tx_id, "latest_ready")
    -    journal.advance(tx_id, "promoting")
    -    journal.advance(
    -        tx_id,
    -        "degraded",
    -        resource="calendar_api@stable:1",
    -        formal_effects=("candidate_started", "old_restore_uncertain"),
    -        error="old endpoint restore failed",
    -        recovery_target="base",
    -    )
    -
    -    action = journal.pending_recovery()[0]
    -    assert action.action == "retry_runtime_recovery"
    -    assert action.phase == "degraded"
    -    with pytest.raises(RuntimeError, match="状态跳转无效"):
    -        journal.advance(tx_id, "complete")
    -    with pytest.raises(RuntimeError, match="缺少 Host retry receipt"):
    -        journal.finish_recovery(action)
    -    with pytest.raises(RuntimeError, match="状态跳转无效"):
    -        journal.advance(tx_id, "recovered")
    -    journal.finish_recovery(
    -        action,
    -        retry_receipt="managed-process-host:calendar:stable:1:runtime-recovered",
    -    )
    -    record = journal.get(tx_id)
    -    assert record.phase == "recovered"
    -    assert record.error == "old endpoint restore failed"
    -    assert record.formal_effects == ("candidate_started", "old_restore_uncertain")
    -    assert journal.pending_recovery() == ()
    -
    -
    -def test_runtime_failure_evidence_upgrades_and_never_loses_owners(
    -    tmp_path: Path,
    -) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="calendar",
    -        base_snapshot_id="stable-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -    journal.advance(
    -        tx_id,
    -        "cleanup_failed",
    -        resource="mcp:calendar",
    -        error="mcp cleanup failed",
    -        recovery_target="base",
    -    )
    -    journal.advance(
    -        tx_id,
    -        "degraded",
    -        resource="process:calendar_api",
    -        error="process watchdog failed",
    -        recovery_target="base",
    -    )
    -
    -    action = journal.pending_recovery()[0]
    -    assert action.phase == "degraded"
    -    assert action.action == "retry_runtime_recovery"
    -    assert action.failure_resource == "mcp:calendar,process:calendar_api"
    -    assert action.attempt_count == 2
    -    with pytest.raises(RuntimeError, match="recovery target 不可覆盖"):
    -        journal.advance(
    -            tx_id,
    -            "degraded",
    -            resource="process:calendar_api",
    -            error="target drift",
    -            recovery_target="candidate",
    -        )
    -
    -
    -def test_cleanup_retry_preserves_a_committed_candidate_target(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="calendar@lab",
    -        base_snapshot_id="stable-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -        base_artifact_pointer=".artifacts/v1",
    -        candidate_artifact_pointer=".artifacts/v2",
    -    )
    -    journal.advance(tx_id, "prepared")
    -    journal.advance(tx_id, "validating")
    -    journal.advance(tx_id, "commit_started")
    -    journal.advance(tx_id, "committed")
    -    journal.advance(tx_id, "draining")
    -    journal.advance(
    -        tx_id,
    -        "cleanup_failed",
    -        resource="composition-runtime:calendar:stable:1",
    -        error="old process group retained",
    -        recovery_target="candidate",
    -    )
    -
    -    action = journal.pending_recovery()[0]
    -    journal.finish_recovery(
    -        action,
    -        retry_receipt="composition-runtime:calendar:stable:1:cleanup-complete",
    -    )
    -
    -    record = journal.get(tx_id)
    -    assert record.phase == "recovered"
    -    assert record.error == "old process group retained"
    -
    -
    -def test_reload_journal_requires_complete_failure_evidence(tmp_path: Path) -> None:
    -    journal = ReloadJournal(tmp_path / "workspace")
    -    tx_id = journal.begin(
    -        plugin_id="calendar",
    -        base_snapshot_id="stable-snapshot-v1",
    -        base_generation_id="calendar:stable:1",
    -        generation_id="calendar:candidate:2",
    -        source_revision="source-v2",
    -        config_revision="config-v2",
    -    )
    -
    -    with pytest.raises(ValueError, match="resource identity"):
    -        journal.advance(
    -            tx_id,
    -            "cleanup_failed",
    -            error="terminate failed",
    -            recovery_target="base",
    -        )
    -    with pytest.raises(ValueError, match="error evidence"):
    -        journal.advance(
    -            tx_id,
    -            "cleanup_failed",
    -            resource="calendar_api@candidate:2",
    -            recovery_target="base",
    -        )
    -    with pytest.raises(ValueError, match="recovery target"):
    -        journal.advance(
    -            tx_id,
    -            "cleanup_failed",
    -            resource="calendar_api@candidate:2",
    -            error="terminate failed",
    -        )
    -
    -    assert journal.get(tx_id).phase == "preparing"
    -
    -
    -def test_reload_journal_migrates_legacy_schema_without_losing_transactions(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    database = workspace / "runtime" / "plugin-reloads.sqlite3"
    -    database.parent.mkdir(parents=True)
    -    conn = sqlite3.connect(database)
    -    try:
    -        conn.executescript(
    -            """
    -            CREATE TABLE reload_transactions (
    -                tx_id TEXT PRIMARY KEY,
    -                plugin_id TEXT NOT NULL,
    -                base_snapshot_id TEXT,
    -                candidate_snapshot_id TEXT,
    -                generation_id TEXT NOT NULL,
    -                source_revision TEXT NOT NULL,
    -                config_revision TEXT NOT NULL,
    -                phase TEXT NOT NULL,
    -                started_at TEXT NOT NULL,
    -                updated_at TEXT NOT NULL,
    -                error TEXT NOT NULL
    -            );
    -            CREATE TABLE reload_events (
    -                sequence INTEGER PRIMARY KEY AUTOINCREMENT,
    -                tx_id TEXT NOT NULL REFERENCES reload_transactions(tx_id),
    -                phase TEXT NOT NULL,
    -                details_json TEXT NOT NULL,
    -                created_at TEXT NOT NULL
    -            );
    -            INSERT INTO reload_transactions VALUES (
    -                'legacy-tx', 'calendar@lab', 'stable-v1', NULL,
    -                'calendar:candidate:2', 'source-v2', 'config-v2',
    -                'prepared', '2026-08-16T00:00:00+00:00',
    -                '2026-08-16T00:00:00+00:00', ''
    -            );
    -            """
    -        )
    -        conn.commit()
    -    finally:
    -        conn.close()
    -
    -    journal = ReloadJournal(workspace)
    -
    -    record = journal.get("legacy-tx")
    -    assert record.phase == "prepared"
    -    assert record.generation_id == "calendar:candidate:2"
    -    assert record.formal_effects == ()
    -    assert record.attempt_count == 0
    -    assert record.runtime_owner_boot_id is None
    -    assert record.base_artifact_pointer is None
    -    assert record.candidate_artifact_pointer is None
    -    assert record.recovery_target is None
    -    action = journal.pending_recovery()[0]
    -    assert action.action == "discard_candidate"
    diff --git a/tests/test_plugin_skill_links.py b/tests/test_plugin_skill_links.py
    deleted file mode 100644
    index 14c7ae7c5..000000000
    --- a/tests/test_plugin_skill_links.py
    +++ /dev/null
    @@ -1,512 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import sqlite3
    -import subprocess
    -import sys
    -from datetime import datetime, timezone
    -from pathlib import Path
    -from typing import cast
    -
    -import pytest
    -
    -from agent.plugins.manager import ActivePluginInfo
    -from agent.plugins.skill_links import PluginSkillLinker
    -from agent.skills import SkillsLoader
    -
    -
    -def _write_plugin_skill(
    -    plugin_root: Path,
    -    plugin_id: str,
    -    skill_name: str,
    -    *,
    -    body: str = "plugin skill body",
    -) -> Path:
    -    skill_dir = plugin_root / plugin_id / "skills" / skill_name
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\n" f"name: {skill_name}\n" "description: 插件技能\n" "---\n" f"{body}\n",
    -        encoding="utf-8",
    -    )
    -    return plugin_root / plugin_id
    -
    -
    -def _write_plugin_drift_skill(
    -    plugin_root: Path,
    -    plugin_id: str,
    -    skill_name: str,
    -    *,
    -    body: str = "plugin drift skill body",
    -) -> Path:
    -    skill_dir = plugin_root / plugin_id / "drift" / "skills" / skill_name
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\n"
    -        f"name: {skill_name}\n"
    -        "description: 插件 Drift 技能\n"
    -        "---\n"
    -        f"{body}\n",
    -        encoding="utf-8",
    -    )
    -    return plugin_root / plugin_id
    -
    -
    -def _plugin_info(
    -    plugin_id: str,
    -    plugin_dir: Path,
    -    manifest: dict[str, object] | None = None,
    -) -> ActivePluginInfo:
    -    return ActivePluginInfo(
    -        plugin_id=plugin_id,
    -        plugin_dir=plugin_dir,
    -        manifest=manifest or {},
    -        module_path=f"test_{plugin_id}",
    -        skill_roots=(
    -            (plugin_dir / "skills",) if (plugin_dir / "skills").is_dir() else ()
    -        ),
    -        drift_skill_roots=(
    -            (plugin_dir / "drift" / "skills",)
    -            if (plugin_dir / "drift" / "skills").is_dir()
    -            else ()
    -        ),
    -    )
    -
    -
    -def test_plugin_skill_linker_creates_workspace_symlink(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -
    -    result = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([_plugin_info("foo", plugin_dir)])
    -
    -    link = workspace / "skills" / "bar"
    -    assert result.expected == 1
    -    assert result.created == 1
    -    assert link.is_symlink()
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -    assert loader.load_skill_body("bar") == "plugin skill body"
    -
    -
    -def test_plugin_skill_linker_removes_stale_link(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("foo", plugin_dir)])
    -
    -    result = linker.sync([])
    -
    -    assert result.removed == 1
    -    assert not (workspace / "skills" / "bar").exists()
    -
    -
    -def test_plugin_skill_linker_preserves_unowned_broken_plugin_link(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    skills_dir = workspace / "skills"
    -    skills_dir.mkdir(parents=True)
    -    link = skills_dir / "gone:bar"
    -    link.symlink_to(plugin_root / "gone" / "skills" / "bar", target_is_directory=True)
    -
    -    result = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([])
    -
    -    assert result.removed == 0
    -    assert link.is_symlink()
    -
    -
    -def test_plugin_skill_linker_rejects_user_skill_dir_without_deleting_it(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -    user_skill = workspace / "skills" / "bar"
    -    user_skill.mkdir(parents=True)
    -    (user_skill / "SKILL.md").write_text("user body", encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="用户文件或目录冲突"):
    -        PluginSkillLinker(
    -            workspace=workspace,
    -            plugin_roots=[plugin_root],
    -        ).sync([_plugin_info("foo", plugin_dir)])
    -
    -    assert user_skill.is_dir()
    -    assert not user_skill.is_symlink()
    -    assert (user_skill / "SKILL.md").read_text(encoding="utf-8") == "user body"
    -
    -
    -def test_plugin_skill_linker_rejects_user_symlink_without_replacing_it(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -    user_target = tmp_path / "personal" / "bar"
    -    user_target.mkdir(parents=True)
    -    (user_target / "SKILL.md").write_text("user body", encoding="utf-8")
    -    user_link = workspace / "skills" / "bar"
    -    user_link.parent.mkdir(parents=True)
    -    user_link.symlink_to(user_target, target_is_directory=True)
    -
    -    with pytest.raises(RuntimeError, match="用户软链接冲突"):
    -        PluginSkillLinker(
    -            workspace=workspace,
    -            plugin_roots=[plugin_root],
    -        ).sync([_plugin_info("foo", plugin_dir)])
    -
    -    assert user_link.is_symlink()
    -    assert user_link.resolve() == user_target
    -
    -
    -def test_plugin_skill_linker_repairs_only_managed_plugin_symlink(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    old_plugin_dir = _write_plugin_skill(plugin_root, "old", "bar", body="old")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("old", old_plugin_dir)])
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -    result = linker.sync([_plugin_info("foo", plugin_dir)])
    -
    -    link = workspace / "skills" / "bar"
    -    assert result.repaired == 1
    -    assert link.resolve() == plugin_dir / "skills" / "bar"
    -
    -
    -def test_plugin_skill_linker_recovers_crash_before_symlink_replace(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    old_dir = _write_plugin_skill(plugin_root, "old", "bar", body="old")
    -    new_dir = _write_plugin_skill(plugin_root, "new", "bar", body="new")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("old", old_dir)])
    -    link = workspace / "skills" / "bar"
    -
    -    monkeypatch.setattr(
    -        linker,
    -        "_replace_link",
    -        lambda _link, _target: (_ for _ in ()).throw(SystemExit("crash")),
    -    )
    -    with pytest.raises(SystemExit, match="crash"):
    -        linker.sync([_plugin_info("new", new_dir)])
    -
    -    recovered = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    assert link.resolve() == old_dir / "skills" / "bar"
    -    result = recovered.sync([_plugin_info("new", new_dir)])
    -    assert result.repaired == 1
    -    assert link.resolve() == new_dir / "skills" / "bar"
    -
    -
    -def test_plugin_skill_linker_recovers_crash_after_symlink_replace(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    old_dir = _write_plugin_skill(plugin_root, "old", "bar", body="old")
    -    new_dir = _write_plugin_skill(plugin_root, "new", "bar", body="new")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("old", old_dir)])
    -    link = workspace / "skills" / "bar"
    -
    -    monkeypatch.setattr(
    -        linker,
    -        "_commit_transition",
    -        lambda _key, _target: (_ for _ in ()).throw(SystemExit("crash")),
    -    )
    -    with pytest.raises(SystemExit, match="crash"):
    -        linker.sync([_plugin_info("new", new_dir)])
    -    assert link.resolve() == new_dir / "skills" / "bar"
    -
    -    recovered = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    result = recovered.sync([_plugin_info("new", new_dir)])
    -    assert result.repaired == 0
    -    assert link.resolve() == new_dir / "skills" / "bar"
    -
    -
    -def test_plugin_skill_linker_rolls_back_after_final_ownership_save_failure(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    old_dir = _write_plugin_skill(plugin_root, "old", "bar", body="old")
    -    new_dir = _write_plugin_skill(plugin_root, "new", "bar", body="new")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("old", old_dir)])
    -    link = workspace / "skills" / "bar"
    -    real_write = linker._write_ownership
    -    write_count = 0
    -
    -    def fail_final_write(owned_links, pending_links) -> None:
    -        nonlocal write_count
    -        write_count += 1
    -        if write_count == 2:
    -            raise OSError("simulated final ownership save failure")
    -        real_write(owned_links, pending_links)
    -
    -    monkeypatch.setattr(linker, "_write_ownership", fail_final_write)
    -    with pytest.raises(OSError, match="final ownership save failure"):
    -        linker.sync([_plugin_info("new", new_dir)])
    -    assert link.resolve() == new_dir / "skills" / "bar"
    -
    -    rollback = linker.sync([_plugin_info("old", old_dir)])
    -    assert rollback.repaired == 1
    -    assert link.resolve() == old_dir / "skills" / "bar"
    -    recovered = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    assert recovered.sync([_plugin_info("old", old_dir)]).repaired == 0
    -
    -
    -def test_plugin_skill_linker_does_not_adopt_user_link_into_plugin_root(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "foo", "bar")
    -    user_link = workspace / "skills" / "bar"
    -    user_link.parent.mkdir(parents=True)
    -    user_link.symlink_to(plugin_dir / "skills" / "bar", target_is_directory=True)
    -
    -    with pytest.raises(RuntimeError, match="用户软链接冲突"):
    -        PluginSkillLinker(
    -            workspace=workspace,
    -            plugin_roots=[plugin_root],
    -        ).sync([_plugin_info("foo", plugin_dir)])
    -
    -    assert user_link.is_symlink()
    -    assert user_link.resolve() == plugin_dir / "skills" / "bar"
    -
    -
    -def test_plugin_skill_linker_does_not_interpret_runtime_policy(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_skill(plugin_root, "akasha", "memory")
    -    manifest: dict[str, object] = {
    -        "skills": {
    -            "enabled_when": {
    -                "kind": "memory_engine",
    -                "engine": "akasha",
    -            }
    -        }
    -    }
    -    plugin = _plugin_info("akasha", plugin_dir, manifest)
    -
    -    disabled = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([plugin])
    -    enabled = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([plugin])
    -    removed = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([plugin])
    -
    -    assert disabled.expected == 1
    -    assert enabled.expected == 1
    -    assert removed.removed == 0
    -    assert (workspace / "skills" / "memory").is_symlink()
    -
    -
    -def test_aka_plugin_skill_is_exposed_with_bare_name(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    cache_root = tmp_path / "cache"
    -    plugin_dir = cache_root / "lab" / "feed" / "0.1.0"
    -    skill_dir = plugin_dir / "skills" / "feed-manage"
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\n" "name: feed-manage\n" "description: feed skill\n" "---\n" "body\n",
    -        encoding="utf-8",
    -    )
    -    plugin = ActivePluginInfo(
    -        plugin_id="feed@lab",
    -        plugin_dir=plugin_dir,
    -        manifest={},
    -        module_path="feed",
    -        skill_roots=(plugin_dir / "skills",),
    -    )
    -
    -    result = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[cache_root],
    -    ).sync([plugin])
    -
    -    assert result.expected == 1
    -    assert (workspace / "skills" / "feed-manage").is_symlink()
    -    assert not (workspace / "skills" / "feed@lab:feed-manage").exists()
    -
    -
    -def test_aka_plugin_skill_sync_preserves_unowned_old_prefixed_link(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    cache_root = tmp_path / "cache"
    -    plugin_dir = cache_root / "lab" / "feed" / "0.1.0"
    -    skill_dir = plugin_dir / "skills" / "feed-manage"
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\n" "name: feed-manage\n" "description: feed skill\n" "---\n" "body\n",
    -        encoding="utf-8",
    -    )
    -    old_link = workspace / "skills" / "feed@lab:feed-manage"
    -    old_link.parent.mkdir(parents=True)
    -    old_link.symlink_to(skill_dir, target_is_directory=True)
    -    plugin = ActivePluginInfo(
    -        plugin_id="feed@lab",
    -        plugin_dir=plugin_dir,
    -        manifest={},
    -        module_path="feed",
    -        skill_roots=(plugin_dir / "skills",),
    -    )
    -
    -    result = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[cache_root],
    -    ).sync([plugin])
    -
    -    assert result.created == 1
    -    assert result.removed == 0
    -    assert (workspace / "skills" / "feed-manage").is_symlink()
    -    assert old_link.is_symlink()
    -
    -
    -def test_aka_plugin_drift_skill_uses_bare_plugin_name(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    cache_root = tmp_path / "cache"
    -    plugin_dir = cache_root / "github" / "emotion" / "0.1.0"
    -    skill_dir = plugin_dir / "drift" / "skills" / "feedback-preference-context"
    -    skill_dir.mkdir(parents=True)
    -    (skill_dir / "SKILL.md").write_text(
    -        "---\n"
    -        "name: feedback-preference-context\n"
    -        "description: drift skill\n"
    -        "---\n"
    -        "body\n",
    -        encoding="utf-8",
    -    )
    -    plugin = ActivePluginInfo(
    -        plugin_id="emotion@github",
    -        plugin_dir=plugin_dir,
    -        manifest={},
    -        module_path="emotion",
    -        drift_skill_roots=(plugin_dir / "drift" / "skills",),
    -    )
    -
    -    result = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[cache_root],
    -    ).sync([plugin])
    -
    -    assert result.expected == 1
    -    assert (workspace / "drift" / "skills" / "feedback-preference-context").is_symlink()
    -    assert not (
    -        workspace / "drift" / "skills" / "emotion:feedback-preference-context"
    -    ).exists()
    -    assert not (
    -        workspace / "drift" / "skills" / "emotion@github:feedback-preference-context"
    -    ).exists()
    -
    -
    -def test_plugin_drift_skill_linker_removes_stale_link(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_drift_skill(plugin_root, "foo", "daily")
    -    linker = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    )
    -    linker.sync([_plugin_info("foo", plugin_dir)])
    -
    -    result = linker.sync([])
    -
    -    assert result.removed == 1
    -    assert not (workspace / "drift" / "skills" / "daily").exists()
    -
    -
    -def test_plugin_drift_skill_linker_rejects_user_skill_dir_without_deleting_it(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_drift_skill(plugin_root, "foo", "daily")
    -    user_skill = workspace / "drift" / "skills" / "daily"
    -    user_skill.mkdir(parents=True)
    -    (user_skill / "SKILL.md").write_text("user body", encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="用户文件或目录冲突"):
    -        PluginSkillLinker(
    -            workspace=workspace,
    -            plugin_roots=[plugin_root],
    -        ).sync([_plugin_info("foo", plugin_dir)])
    -
    -    assert user_skill.is_dir()
    -    assert not user_skill.is_symlink()
    -    assert (user_skill / "SKILL.md").read_text(encoding="utf-8") == "user body"
    -
    -
    -def test_plugin_drift_skill_linker_does_not_interpret_runtime_policy(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugins"
    -    plugin_dir = _write_plugin_drift_skill(plugin_root, "akasha", "daily")
    -    manifest: dict[str, object] = {
    -        "drift_skills": {
    -            "enabled_when": {
    -                "kind": "memory_engine",
    -                "engine": "akasha",
    -            }
    -        }
    -    }
    -    plugin = _plugin_info("akasha", plugin_dir, manifest)
    -
    -    disabled = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([plugin])
    -    enabled = PluginSkillLinker(
    -        workspace=workspace,
    -        plugin_roots=[plugin_root],
    -    ).sync([plugin])
    -
    -    assert disabled.expected == 1
    -    assert enabled.expected == 1
    -    assert (workspace / "drift" / "skills" / "daily").is_symlink()
    diff --git a/tests/test_plugin_source_resolver.py b/tests/test_plugin_source_resolver.py
    deleted file mode 100644
    index 18354c210..000000000
    --- a/tests/test_plugin_source_resolver.py
    +++ /dev/null
    @@ -1,173 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugins.artifacts import ArtifactPointer, write_pointers
    -from agent.plugins.source_resolver import resolve_plugin_sources
    -
    -
    -def _write_artifact(plugin_base: Path, artifact_id: str) -> Path:
    -    artifact = plugin_base / ".artifacts" / artifact_id
    -    artifact.mkdir(parents=True)
    -    (artifact / "plugin.py").write_text("", encoding="utf-8")
    -    (artifact / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        'name = "feed"\n'
    -        'version = "3.0.0"\n'
    -        "api_version = 3\n"
    -        'entrypoint = "plugin.py"\n',
    -        encoding="utf-8",
    -    )
    -    return artifact
    -
    -
    -def test_installed_resolver_does_not_follow_cache_symlinks(tmp_path: Path) -> None:
    -    cache = tmp_path / "cache"
    -    outside = tmp_path / "outside" / "feed" / "1.0.0"
    -    outside.mkdir(parents=True)
    -    (outside / "plugin.py").write_text("", encoding="utf-8")
    -
    -    (cache / "lab").mkdir(parents=True)
    -    (cache / "lab" / "feed").symlink_to(outside.parent, target_is_directory=True)
    -    (cache / "lab" / "safe" / "1.0.0").mkdir(parents=True)
    -    (cache / "lab" / "safe" / "1.0.0" / "plugin.py").write_text(
    -        "",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="符号链接"):
    -        resolve_plugin_sources([], installed_cache_root=cache)
    -
    -
    -def test_installed_resolver_ignores_transaction_directories(tmp_path: Path) -> None:
    -    cache = tmp_path / "cache" / "lab" / "feed"
    -    (cache / ".1.0.0-backup-123").mkdir(parents=True)
    -    (cache / ".1.0.0-backup-123" / "plugin.py").write_text("", encoding="utf-8")
    -    (cache / ".feed-install-123").mkdir(parents=True)
    -    (cache / ".feed-install-123" / "plugin.py").write_text("", encoding="utf-8")
    -
    -    assert resolve_plugin_sources([], installed_cache_root=tmp_path / "cache") == []
    -
    -
    -def test_installed_resolver_rejects_legacy_visible_versions(tmp_path: Path) -> None:
    -    plugin_root = tmp_path / "cache" / "lab" / "feed"
    -    for version in ("1.0.0", "2.0.0"):
    -        (plugin_root / version).mkdir(parents=True)
    -        (plugin_root / version / "plugin.py").write_text("", encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match="不受支持的旧版可见目录"):
    -        resolve_plugin_sources([], installed_cache_root=tmp_path / "cache")
    -
    -
    -def test_installed_resolver_rejects_missing_static_manifest(tmp_path: Path) -> None:
    -    plugin_base = tmp_path / "cache" / "lab" / "feed"
    -    artifact = plugin_base / ".artifacts" / "1.0.0-aaaa"
    -    artifact.mkdir(parents=True)
    -    (artifact / "plugin.py").write_text("", encoding="utf-8")
    -    (plugin_base / ".pointers.json").write_text(
    -        '{"stable":".artifacts/1.0.0-aaaa",'
    -        '"latest":".artifacts/1.0.0-aaaa"}\n',
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="缺少静态 manifest"):
    -        resolve_plugin_sources([], installed_cache_root=tmp_path / "cache")
    -
    -
    -def test_installed_resolver_selects_stable_or_latest_artifact(tmp_path: Path) -> None:
    -    plugin_base = tmp_path / "cache" / "lab" / "feed"
    -    stable = _write_artifact(plugin_base, "1.0.0-aaaa")
    -    latest = _write_artifact(plugin_base, "2.0.0-bbbb")
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -        latest=ArtifactPointer(".artifacts/2.0.0-bbbb"),
    -    )
    -
    -    stable_sources = resolve_plugin_sources(
    -        [],
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    latest_sources = resolve_plugin_sources(
    -        [],
    -        installed_cache_root=tmp_path / "cache",
    -        installed_selector="latest",
    -    )
    -
    -    assert [(item.plugin_name, item.plugin_root) for item in stable_sources] == [
    -        ("feed", stable)
    -    ]
    -    assert [(item.plugin_name, item.plugin_root) for item in latest_sources] == [
    -        ("feed", latest)
    -    ]
    -
    -
    -def test_installed_resolver_allows_candidate_before_first_promotion(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "cache" / "lab" / "feed"
    -    latest = _write_artifact(plugin_base, "1.0.0-aaaa")
    -    _ = write_pointers(
    -        plugin_base,
    -        stable=ArtifactPointer(None),
    -        latest=ArtifactPointer(".artifacts/1.0.0-aaaa"),
    -    )
    -
    -    assert (
    -        resolve_plugin_sources(
    -            [],
    -            installed_cache_root=tmp_path / "cache",
    -        )
    -        == []
    -    )
    -    assert (
    -        resolve_plugin_sources(
    -            [],
    -            installed_cache_root=tmp_path / "cache",
    -            installed_selector="latest",
    -        )[0].plugin_root
    -        == latest
    -    )
    -
    -
    -def test_installed_resolver_rejects_invalid_or_escaping_pointer_state(
    -    tmp_path: Path,
    -) -> None:
    -    plugin_base = tmp_path / "cache" / "lab" / "feed"
    -    _ = _write_artifact(plugin_base, "1.0.0-aaaa")
    -    (plugin_base / ".pointers.json").write_text(
    -        '{"stable":".artifacts/1.0.0-aaaa"}\n',
    -        encoding="utf-8",
    -    )
    -    with pytest.raises(ValueError, match="结构无效"):
    -        resolve_plugin_sources([], installed_cache_root=tmp_path / "cache")
    -
    -    (plugin_base / ".pointers.json").write_text(
    -        '{"stable":".artifacts/1.0.0-aaaa","latest":"../outside"}\n',
    -        encoding="utf-8",
    -    )
    -    with pytest.raises(ValueError, match="pointer 越界"):
    -        resolve_plugin_sources(
    -            [],
    -            installed_cache_root=tmp_path / "cache",
    -            installed_selector="latest",
    -        )
    -
    -
    -def test_installed_resolver_rejects_artifact_symlink(tmp_path: Path) -> None:
    -    plugin_base = tmp_path / "cache" / "lab" / "feed"
    -    outside = tmp_path / "outside"
    -    outside.mkdir()
    -    (outside / "plugin.py").write_text("", encoding="utf-8")
    -    artifacts = plugin_base / ".artifacts"
    -    artifacts.mkdir(parents=True)
    -    (artifacts / "escape").symlink_to(outside, target_is_directory=True)
    -    (plugin_base / ".pointers.json").write_text(
    -        '{"stable":".artifacts/escape","latest":".artifacts/escape"}\n',
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="不能经过符号链接"):
    -        resolve_plugin_sources([], installed_cache_root=tmp_path / "cache")
    diff --git a/tests/test_plugin_static_manifest.py b/tests/test_plugin_static_manifest.py
    deleted file mode 100644
    index 37657227a..000000000
    --- a/tests/test_plugin_static_manifest.py
    +++ /dev/null
    @@ -1,744 +0,0 @@
    -from __future__ import annotations
    -
    -import subprocess
    -import sys
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import cast
    -
    -import pytest
    -
    -import agent.plugins.install as install_module
    -import agent.plugins.manager as manager_module
    -from agent.plugins.generation import PluginGeneration
    -from agent.plugins.snapshot import RuntimeSnapshot
    -from agent.plugins.install import install_git_plugin
    -from agent.plugins.static_manifest import (
    -    load_static_plugin_manifest,
    -    materialize_static_command,
    -    staged_python_interpreter,
    -)
    -
    -
    -def _git(root: Path, *args: str) -> str:
    -    result = subprocess.run(
    -        ["git", *args],
    -        cwd=root,
    -        capture_output=True,
    -        text=True,
    -        check=True,
    -    )
    -    return result.stdout.strip()
    -
    -
    -def _commit(root: Path) -> None:
    -    _git(root, "init", "--quiet")
    -    _git(root, "config", "user.email", "test@example.invalid")
    -    _git(root, "config", "user.name", "test")
    -    _git(root, "add", ".")
    -    _git(root, "commit", "--quiet", "-m", "fixture")
    -
    -
    -def _manifest(
    -    *,
    -    name: str = "calendar",
    -    version: str = "3.0.0",
    -    entrypoint: str = "plugin.py",
    -    requirements: str = "mcp/requirements.txt",
    -) -> str:
    -    return (
    -        "schema_version = 1\n"
    -        f'name = "{name}"\n'
    -        f'version = "{version}"\n'
    -        "api_version = 3\n"
    -        f'entrypoint = "{entrypoint}"\n\n'
    -        "[[python]]\n"
    -        f'requirements = "{requirements}"\n\n'
    -        "[validation]\n"
    -        'exclude_data_paths = [".env", "token.json"]\n'
    -    )
    -
    -
    -def test_static_manifest_is_import_free_and_exposes_runtime_policy(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text(
    -        "raise RuntimeError('must not import during static parse')\n",
    -        encoding="utf-8",
    -    )
    -    (root / "mcp" / "requirements.txt").write_text("requests\n", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(_manifest(), encoding="utf-8")
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.name == "calendar"
    -    assert manifest.version == "3.0.0"
    -    assert manifest.entrypoint == "plugin.py"
    -    assert manifest.requirements == ("mcp/requirements.txt",)
    -    assert manifest.python[0].runtime_root == "mcp"
    -    assert manifest.exclude_data_paths == (".env", "token.json")
    -    assert len(manifest.identity_digest) == 64
    -
    -
    -def test_static_manifest_rejects_removed_candidate_data_mode(tmp_path: Path) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    manifest_path = root / "akashic.plugin.toml"
    -    manifest_path.write_text(
    -        _manifest().replace(
    -            'entrypoint = "plugin.py"\n\n',
    -            'entrypoint = "plugin.py"\ncandidate_data_mode = "shared_read"\n\n',
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="未知字段.*candidate_data_mode"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_freezes_channel_credential_paths_before_import(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "feishu"
    -    root.mkdir()
    -    (root / "plugin.py").write_text(
    -        "raise RuntimeError('candidate must not import during static parse')\n",
    -        encoding="utf-8",
    -    )
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'feishu'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[channel_credentials]\n"
    -        "feishu = ['app_secret', 'oauth.token']\n",
    -        encoding="utf-8",
    -    )
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.channel_credentials == (
    -        ("feishu", ("app_secret", "oauth.token")),
    -    )
    -
    -
    -@pytest.mark.parametrize(
    -    "paths",
    -    (
    -        "['oauth', 'oauth.token']",
    -        "['bad..path']",
    -        "['UPPER']",
    -    ),
    -)
    -def test_static_manifest_rejects_ambiguous_channel_credential_paths(
    -    tmp_path: Path,
    -    paths: str,
    -) -> None:
    -    root = tmp_path / "feishu"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'feishu'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[channel_credentials]\n"
    -        f"feishu = {paths}\n",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="channel_credentials"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_rejects_cross_channel_credential_path_overlap(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "channels"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'channels'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[channel_credentials]\n"
    -        "feishu = ['oauth']\n"
    -        "qqbot = ['oauth.token']\n",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="跨 channel 路径重叠"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_validates_mcp_and_process_declarations(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "run_mcp.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "run_server.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + """
    -[[processes]]
    -name = "calendar_api"
    -command = ["python", "mcp/run_server.py"]
    -cwd = "mcp"
    -port_env = "PORT"
    -formal_port = 18000
    -readiness_path = "/health"
    -
    -[[mcp]]
    -name = "calendar"
    -command = ["python", "mcp/run_mcp.py"]
    -cwd = "mcp"
    -required_tools = ["list_events"]
    -candidate_read_only_tools = ["list_events"]
    -endpoint_env = [{env = "PORT", process = "calendar_api"}]
    -candidate_env = {CALENDAR_BACKEND = "recording"}
    -""",
    -        encoding="utf-8",
    -    )
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.managed_processes[0].name == "calendar_api"
    -    assert manifest.managed_processes[0].formal_port == 18000
    -    assert manifest.mcp_servers[0].endpoint_env == (("PORT", "calendar_api"),)
    -    assert manifest.mcp_servers[0].candidate_env == (("CALENDAR_BACKEND", "recording"),)
    -    assert manifest.mcp_servers[0].python_runtime == "mcp"
    -    assert manifest.managed_processes[0].python_runtime == "mcp"
    -
    -
    -@pytest.mark.parametrize(
    -    "removed_field",
    -    ("mcp_servers", "process", "managed_processes", "workloads"),
    -)
    -def test_static_manifest_rejects_removed_v2_declaration_alias(
    -    tmp_path: Path,
    -    removed_field: str,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + f"\n[[{removed_field}]]\nname = 'legacy'\ncommand = ['run.py']\n",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="未知字段"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_validates_relative_command_head_inside_artifact(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "run_mcp.py").write_text("", encoding="utf-8")
    -    runner = root / "mcp" / "runner"
    -    runner.write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + '\n[[mcp]]\nname = "calendar"\ncommand = ["mcp/runner", "mcp/run_mcp.py"]\n',
    -        encoding="utf-8",
    -    )
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.mcp_servers[0].command == ("mcp/runner", "mcp/run_mcp.py")
    -
    -    (root / "mcp" / ".venv" / "bin").mkdir(parents=True)
    -    interpreter = root / "mcp" / ".venv" / "bin" / "python"
    -    interpreter.write_text("", encoding="utf-8")
    -    interpreter.chmod(interpreter.stat().st_mode | 0o111)
    -    assert staged_python_interpreter(root, manifest.python[0]) == interpreter
    -
    -
    -def test_static_manifest_rejects_external_command_argument(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "run_mcp.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + f'\n[[mcp]]\nname = "calendar"\ncommand = ["{sys.executable}", "/tmp/other.py"]\n',
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="artifact 外绝对路径"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_rejects_escaped_relative_command_head(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    outside = tmp_path / "outside"
    -    outside.write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + '\n[[mcp]]\nname = "calendar"\ncommand = ["../outside"]\n',
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="artifact 内的相对路径"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_candidate_data_inventory_excludes_manifest_paths(tmp_path: Path) -> None:
    -    source = tmp_path / "production-data"
    -    target = tmp_path / "candidate-data"
    -    source.mkdir()
    -    (source / "state.json").write_text("keep", encoding="utf-8")
    -    (source / ".env").write_text("SECRET=bad", encoding="utf-8")
    -    (source / "oauth").mkdir()
    -    (source / "oauth" / "token.json").write_text("secret", encoding="utf-8")
    -
    -    inventory = manager_module._copy_validation_data(  # pyright: ignore[reportPrivateUsage]
    -        source,
    -        target,
    -        (".env", "oauth"),
    -    )
    -
    -    assert inventory == ("state.json",)
    -    assert (target / "state.json").is_file()
    -    assert not (target / ".env").exists()
    -    assert not (target / "oauth").exists()
    -
    -
    -def test_candidate_data_copy_rejects_symlink_to_formal_storage(
    -    tmp_path: Path,
    -) -> None:
    -    source = tmp_path / "production-data"
    -    target = tmp_path / "candidate-data"
    -    outside = tmp_path / "formal-secret"
    -    source.mkdir()
    -    outside.write_text("secret", encoding="utf-8")
    -    (source / "token-link").symlink_to(outside)
    -
    -    with pytest.raises(RuntimeError, match="不允许复制符号链接"):
    -        manager_module._copy_validation_data(  # pyright: ignore[reportPrivateUsage]
    -            source,
    -            target,
    -            (),
    -        )
    -
    -    assert not target.exists()
    -
    -
    -def test_candidate_data_copy_ignores_symlink_inside_excluded_cache(
    -    tmp_path: Path,
    -) -> None:
    -    source = tmp_path / "production-data"
    -    target = tmp_path / "candidate-data"
    -    cache = source / "checkouts" / "repository" / ".venv"
    -    cache.mkdir(parents=True)
    -    (cache / "lib").mkdir()
    -    (cache / "lib64").symlink_to("lib", target_is_directory=True)
    -    (source / "state.json").write_text("keep", encoding="utf-8")
    -
    -    inventory = manager_module._copy_validation_data(  # pyright: ignore[reportPrivateUsage]
    -        source,
    -        target,
    -        ("checkouts",),
    -    )
    -
    -    assert inventory == ("state.json",)
    -    assert not (target / "checkouts").exists()
    -
    -
    -def test_static_process_declaration_must_match_c13_root_registry(
    -    tmp_path: Path,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "run_server.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'calendar'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[[processes]]\n"
    -        "name = 'calendar_api'\n"
    -        "command = ['run_server.py']\n"
    -        "port_env = 'PORT'\n"
    -        "formal_port = 18000\n",
    -        encoding="utf-8",
    -    )
    -    manifest = load_static_plugin_manifest(root)
    -    generation = SimpleNamespace(static_manifest=manifest, plugin_dir=root)
    -    snapshot = SimpleNamespace(
    -        mcp_server_registry=None,
    -        managed_process_registry=None,
    -        composition_active_plugin_ids=frozenset({"calendar"}),
    -    )
    -
    -    with pytest.raises(RuntimeError, match="managed process 声明"):
    -        manager_module._validate_static_manifest_runtime(  # pyright: ignore[reportPrivateUsage]
    -            cast(RuntimeSnapshot, snapshot),
    -            {"calendar": cast(PluginGeneration, generation)},
    -        )
    -
    -
    -def test_static_python_command_uses_only_staged_interpreter(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "run.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        _manifest()
    -        + '\n[[mcp]]\nname = "calendar"\ncommand = ["python", "mcp/run.py"]\n',
    -        encoding="utf-8",
    -    )
    -    interpreter = root / "mcp" / ".venv" / "bin" / "python"
    -    interpreter.parent.mkdir(parents=True)
    -    interpreter.write_text("", encoding="utf-8")
    -    interpreter.chmod(0o755)
    -    hostile = tmp_path / "hostile"
    -    hostile.mkdir()
    -    (hostile / "python").write_text("", encoding="utf-8")
    -    monkeypatch.setenv("PATH", str(hostile))
    -
    -    manifest = load_static_plugin_manifest(root)
    -    command = materialize_static_command(root, manifest, manifest.mcp_servers[0])
    -
    -    assert command == (str(interpreter), "mcp/run.py")
    -
    -
    -def test_static_artifact_command_uses_absolute_executable(tmp_path: Path) -> None:
    -    root = tmp_path / "computer"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    executable = root / "mcp_server.py"
    -    executable.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
    -    executable.chmod(0o755)
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'computer'\n"
    -        "version = '1.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[[mcp]]\n"
    -        "name = 'computer'\n"
    -        "command = ['mcp_server.py']\n",
    -        encoding="utf-8",
    -    )
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert materialize_static_command(
    -        root, manifest, manifest.mcp_servers[0]
    -    ) == (str(executable),)
    -
    -
    -@pytest.mark.parametrize("python_command", ("python", "python3.12"))
    -def test_static_python_command_requires_unique_declared_runtime(
    -    tmp_path: Path,
    -    python_command: str,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "run.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'calendar'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[[mcp]]\n"
    -        "name = 'calendar'\n"
    -        f"command = ['{python_command}', 'run.py']\n",
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="唯一绑定"):
    -        load_static_plugin_manifest(root)
    -
    -
    -@pytest.mark.parametrize(
    -    ("field", "value", "message"),
    -    [
    -        ("entrypoint", "/plugin.py", "entrypoint"),
    -        ("requirements", "../requirements.txt", "requirements"),
    -        ("requirements", "missing.txt", "requirements"),
    -    ],
    -)
    -def test_static_manifest_rejects_unsafe_or_missing_runtime_paths(
    -    tmp_path: Path,
    -    field: str,
    -    value: str,
    -    message: str,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    (root / "mcp").mkdir(parents=True)
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp" / "requirements.txt").write_text("", encoding="utf-8")
    -    manifest_text = _manifest(
    -        entrypoint=value if field == "entrypoint" else "plugin.py",
    -        requirements=value if field == "requirements" else "mcp/requirements.txt",
    -    )
    -    (root / "akashic.plugin.toml").write_text(manifest_text, encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match=message):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_manifest_rejects_manifest_symlink(tmp_path: Path) -> None:
    -    root = tmp_path / "calendar"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    outside = tmp_path / "outside.toml"
    -    outside.write_text(_manifest(requirements="requirements.txt"), encoding="utf-8")
    -    (root / "akashic.plugin.toml").symlink_to(outside)
    -
    -    with pytest.raises(ValueError, match="缺少静态 manifest"):
    -        load_static_plugin_manifest(root)
    -
    -
    -@pytest.mark.parametrize(
    -    "declaration",
    -    (
    -        "[validation]\nexclude_data_paths = ['.']\n",
    -        "[[processes]]\nname = 'api'\ncommand = ['run.py']\n"
    -        "port_env = 'AKASHIC_WORKSPACE'\nformal_port = 18000\n",
    -        "[[processes]]\nname = 'api'\ncommand = ['run.py']\n"
    -        "port_env = 'PORT'\nformal_port = 18000\nreadiness_path = '//evil'\n",
    -        "[[processes]]\nname = 'api'\ncommand = ['run.py']\n"
    -        "port_env = 'PORT'\nformal_port = 18000\nstartup_timeout_seconds = nan\n",
    -        "[[processes]]\nname = 'api'\ncommand = ['run.py']\n",
    -        "[[processes]]\nname = 'api'\ncommand = ['run.py']\n"
    -        "port_env = 'PORT'\nformal_port = 18000\n\n"
    -        "[[mcp]]\nname = 'calendar'\ncommand = ['run.py']\n"
    -        "endpoint_env = [{env = 'AKASHIC_WORKSPACE', process = 'api'}]\n",
    -    ),
    -)
    -def test_static_manifest_rejects_invalid_runtime_policy_before_import(
    -    tmp_path: Path,
    -    declaration: str,
    -) -> None:
    -    root = tmp_path / "calendar"
    -    root.mkdir()
    -    marker = root / "imported"
    -    (root / "plugin.py").write_text(
    -        "from pathlib import Path\n"
    -        "Path(__file__).with_name('imported').write_text('bad')\n",
    -        encoding="utf-8",
    -    )
    -    (root / "run.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'calendar'\n"
    -        "version = '3.0.0'\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        + declaration,
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError):
    -        load_static_plugin_manifest(root)
    -    assert not marker.exists()
    -
    -
    -def test_v3_static_install_stages_before_importing_plugin(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    repo = tmp_path / "calendar-source"
    -    (repo / "mcp").mkdir(parents=True)
    -    (repo / "plugin.py").write_text(
    -        "from pathlib import Path\n"
    -        "Path(__file__).with_name('imported').write_text('bad')\n"
    -        "raise RuntimeError('static install must not import')\n",
    -        encoding="utf-8",
    -    )
    -    (repo / "mcp" / "requirements.txt").write_text("requests\n", encoding="utf-8")
    -    (repo / "akashic.plugin.toml").write_text(_manifest(), encoding="utf-8")
    -    _commit(repo)
    -    calls: list[tuple[str, Path]] = []
    -
    -    def fake_run(args: list[str], *, cwd: Path, label: str) -> None:
    -        calls.append((label, cwd))
    -        if label.endswith("venv"):
    -            python_path = install_module._venv_python_path(cwd / ".venv")
    -            python_path.parent.mkdir(parents=True, exist_ok=True)
    -            python_path.write_text("", encoding="utf-8")
    -
    -    monkeypatch.setattr(install_module, "_run_command", fake_run)
    -    result = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(repo),
    -        marketplace="lab",
    -        plugins_home=tmp_path / "plugins-home",
    -    )
    -
    -    assert result.plugin_name == "calendar"
    -    assert result.plugin_version == "3.0.0"
    -    assert [label for label, _ in calls] == [
    -        "calendar python[0] venv",
    -        "calendar python[0] pip install",
    -    ]
    -    assert not (result.installed_path / "imported").exists()
    -    assert (result.installed_path / "akashic.plugin.toml").is_file()
    -    assert (result.installed_path / "mcp" / ".venv").is_dir()
    -
    -
    -def test_v3_static_install_accepts_custom_entrypoint(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    repo = tmp_path / "calendar-source"
    -    repo.mkdir()
    -    (repo / "entry.py").write_text("", encoding="utf-8")
    -    (repo / "requirements.txt").write_text("", encoding="utf-8")
    -    (repo / "akashic.plugin.toml").write_text(
    -        _manifest(entrypoint="entry.py", requirements="requirements.txt"),
    -        encoding="utf-8",
    -    )
    -    _commit(repo)
    -
    -    def fake_run(args: list[str], *, cwd: Path, label: str) -> None:
    -        if label.endswith("venv"):
    -            python_path = install_module._venv_python_path(cwd / ".venv")
    -            python_path.parent.mkdir(parents=True, exist_ok=True)
    -            python_path.write_text("", encoding="utf-8")
    -
    -    monkeypatch.setattr(install_module, "_run_command", fake_run)
    -    result = install_git_plugin(
    -        workspace=tmp_path / "workspace",
    -        source=str(repo),
    -        marketplace="lab",
    -        plugins_home=tmp_path / "plugins-home",
    -    )
    -
    -    assert (result.installed_path / "entry.py").is_file()
    -    assert not (result.installed_path / "plugin.py").exists()
    -
    -
    -def test_install_rejects_missing_static_manifest_before_plugin_import(
    -    tmp_path: Path,
    -) -> None:
    -    repo = tmp_path / "legacy-source"
    -    repo.mkdir()
    -    imported = tmp_path / "imported"
    -    (repo / "plugin.py").write_text(
    -        "from pathlib import Path\n"
    -        f"Path({str(imported)!r}).write_text('imported', encoding='utf-8')\n",
    -        encoding="utf-8",
    -    )
    -    _commit(repo)
    -
    -    with pytest.raises(ValueError, match="akashic.plugin.toml"):
    -        install_git_plugin(
    -            workspace=tmp_path / "workspace",
    -            source=str(repo),
    -            marketplace="lab",
    -            plugins_home=tmp_path / "plugins-home",
    -        )
    -
    -    assert not imported.exists()
    -    assert not (tmp_path / "workspace" / "plugin-data").exists()
    -
    -
    -@pytest.mark.parametrize("preexisting", (False, True))
    -def test_v3_static_staging_failure_preserves_formal_data_state(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    preexisting: bool,
    -) -> None:
    -    repo = tmp_path / "calendar-source"
    -    repo.mkdir()
    -    (repo / "plugin.py").write_text("", encoding="utf-8")
    -    (repo / "requirements.txt").write_text("", encoding="utf-8")
    -    (repo / "akashic.plugin.toml").write_text(
    -        _manifest(requirements="requirements.txt"),
    -        encoding="utf-8",
    -    )
    -    _commit(repo)
    -    workspace = tmp_path / "workspace"
    -    data_path = workspace / "plugin-data" / "calendar-lab"
    -    if preexisting:
    -        data_path.mkdir(parents=True)
    -        (data_path / "state.json").write_bytes(b"keep")
    -
    -    def fail_run(args: list[str], *, cwd: Path, label: str) -> None:
    -        raise RuntimeError("staging failed")
    -
    -    monkeypatch.setattr(install_module, "_run_command", fail_run)
    -    with pytest.raises(RuntimeError, match="staging failed"):
    -        install_git_plugin(
    -            workspace=workspace,
    -            source=str(repo),
    -            marketplace="lab",
    -            plugins_home=tmp_path / "plugins-home",
    -        )
    -
    -    if preexisting:
    -        assert (data_path / "state.json").read_bytes() == b"keep"
    -    else:
    -        assert not data_path.exists()
    -
    -
    -def test_v3_static_manifest_write_failure_removes_new_data_dir(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    repo = tmp_path / "calendar-source"
    -    repo.mkdir()
    -    (repo / "plugin.py").write_text("", encoding="utf-8")
    -    (repo / "requirements.txt").write_text("", encoding="utf-8")
    -    (repo / "akashic.plugin.toml").write_text(
    -        _manifest(requirements="requirements.txt"),
    -        encoding="utf-8",
    -    )
    -    _commit(repo)
    -    workspace = tmp_path / "workspace"
    -
    -    def fake_run(args: list[str], *, cwd: Path, label: str) -> None:
    -        if label.endswith("venv"):
    -            python_path = install_module._venv_python_path(cwd / ".venv")
    -            python_path.parent.mkdir(parents=True, exist_ok=True)
    -            python_path.write_text("", encoding="utf-8")
    -
    -    def fail_manifest(*args: object, **kwargs: object) -> Path:
    -        raise OSError("manifest write failed")
    -
    -    monkeypatch.setattr(install_module, "_run_command", fake_run)
    -    monkeypatch.setattr(install_module, "upsert_plugin_manifest", fail_manifest)
    -    with pytest.raises(OSError, match="manifest write failed"):
    -        install_git_plugin(
    -            workspace=workspace,
    -            source=str(repo),
    -            marketplace="lab",
    -            plugins_home=tmp_path / "plugins-home",
    -        )
    -
    -    assert not (workspace / "plugin-data" / "calendar-lab").exists()
    diff --git a/tests/test_plugin_trusted_install.py b/tests/test_plugin_trusted_install.py
    deleted file mode 100644
    index db37df338..000000000
    --- a/tests/test_plugin_trusted_install.py
    +++ /dev/null
    @@ -1,525 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import os
    -import shutil
    -import subprocess
    -import sys
    -from pathlib import Path
    -from types import SimpleNamespace
    -
    -import pytest
    -
    -import main
    -import agent.plugins.install as plugin_install_module
    -from agent.supervisor import _SupervisorLock
    -from agent.plugins.artifacts import read_pointers
    -from agent.plugins.trusted_install import (
    -    install_trusted_plugin_batch,
    -    load_trusted_plugin_batch,
    -)
    -from bootstrap.tools import CoreRuntime
    -from bootstrap.workspace_lock import (
    -    PluginPublicationLock,
    -    WorkspaceInstanceLock,
    -    WorkspaceMaintenanceLock,
    -)
    -
    -
    -def test_trusted_batch_installs_exact_v3_plugins_as_stable(tmp_path: Path) -> None:
    -    repositories = [
    -        _create_plugin_repository(tmp_path / "citation", "citation"),
    -        _create_plugin_repository(tmp_path / "steam", "steam"),
    -    ]
    -    batch_path = tmp_path / "trusted.json"
    -    batch_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": str(repository),
    -                        "marketplace": "lab",
    -                        "ref": _git_output(repository, "rev-parse", "HEAD"),
    -                    }
    -                    for repository in repositories
    -                ],
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    home = tmp_path / "plugins-home"
    -    receipt = install_trusted_plugin_batch(
    -        workspace=tmp_path / "workspace",
    -        batch_path=batch_path,
    -        plugins_home=home,
    -    )
    -
    -    assert receipt["mode"] == "operator_trusted_offline_batch"
    -    assert receipt["programmaticValidation"] == "bypassed_by_operator_trust"
    -    installed = receipt["plugins"]
    -    assert isinstance(installed, list)
    -    assert [item["pluginId"] for item in installed] == [
    -        "citation@lab",
    -        "steam@lab",
    -    ]
    -    for item in installed:
    -        plugin_name = str(item["pluginId"]).split("@", maxsplit=1)[0]
    -        pointers = read_pointers(home / "cache" / "lab" / plugin_name)
    -        assert pointers is not None
    -        assert pointers.stable == pointers.latest
    -        assert str(item["sourceRevision"])[:16] in str(item["installedPath"])
    -
    -
    -def test_trusted_batch_restages_an_existing_exact_artifact(
    -    tmp_path: Path,
    -) -> None:
    -    repository = _create_plugin_repository(
    -        tmp_path / "calendar",
    -        "calendar",
    -        python_runtime=True,
    -    )
    -    revision = _git_output(repository, "rev-parse", "HEAD")
    -    batch_path = tmp_path / "trusted.json"
    -    batch_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": str(repository),
    -                        "marketplace": "lab",
    -                        "ref": revision,
    -                    }
    -                ],
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -    home = tmp_path / "plugins-home"
    -    first = install_trusted_plugin_batch(
    -        workspace=tmp_path / "workspace",
    -        batch_path=batch_path,
    -        plugins_home=home,
    -    )
    -    artifact = Path(first["plugins"][0]["installedPath"])  # type: ignore[index]
    -    runtime = artifact / "mcp/.venv"
    -    assert runtime.is_dir()
    -    shutil.rmtree(runtime)
    -
    -    second = install_trusted_plugin_batch(
    -        workspace=tmp_path / "workspace",
    -        batch_path=batch_path,
    -        plugins_home=home,
    -    )
    -
    -    refreshed = Path(second["plugins"][0]["installedPath"])  # type: ignore[index]
    -    assert refreshed != artifact
    -    assert not artifact.exists()
    -    assert (refreshed / "mcp/.venv/bin/python").is_file()
    -
    -
    -def test_trusted_batch_keeps_old_artifact_when_pointer_commit_fails(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    repository = _create_plugin_repository(
    -        tmp_path / "calendar",
    -        "calendar",
    -        python_runtime=True,
    -    )
    -    revision = _git_output(repository, "rev-parse", "HEAD")
    -    batch_path = tmp_path / "trusted.json"
    -    batch_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": str(repository),
    -                        "marketplace": "lab",
    -                        "ref": revision,
    -                    }
    -                ],
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -    home = tmp_path / "plugins-home"
    -    first = install_trusted_plugin_batch(
    -        workspace=tmp_path / "workspace",
    -        batch_path=batch_path,
    -        plugins_home=home,
    -    )
    -    artifact = Path(first["plugins"][0]["installedPath"])  # type: ignore[index]
    -    plugin_base = home / "cache/lab/calendar"
    -    previous_pointers = read_pointers(plugin_base)
    -    real_write_pointers = plugin_install_module.write_pointers
    -    calls = 0
    -
    -    def fail_first_pointer_commit(*args: object, **kwargs: object) -> Path:
    -        nonlocal calls
    -        calls += 1
    -        if calls == 1:
    -            raise OSError("simulated pointer commit failure")
    -        return real_write_pointers(*args, **kwargs)  # type: ignore[arg-type]
    -
    -    monkeypatch.setattr(
    -        plugin_install_module,
    -        "write_pointers",
    -        fail_first_pointer_commit,
    -    )
    -
    -    with pytest.raises(RuntimeError, match="simulated pointer commit failure"):
    -        install_trusted_plugin_batch(
    -            workspace=tmp_path / "workspace",
    -            batch_path=batch_path,
    -            plugins_home=home,
    -        )
    -
    -    assert artifact.is_dir()
    -    assert read_pointers(plugin_base) == previous_pointers
    -    assert list((plugin_base / ".artifacts").iterdir()) == [artifact]
    -
    -
    -@pytest.mark.parametrize(
    -    "payload, message",
    -    [
    -        (
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": "https://example.invalid/plugin.git",
    -                        "marketplace": "github",
    -                        "ref": "main",
    -                    }
    -                ],
    -            },
    -            "完整 commit SHA",
    -        ),
    -        (
    -            {"schema_version": 1, "plugins": [], "future": True},
    -            "只接受 schema_version 和 plugins",
    -        ),
    -    ],
    -)
    -def test_trusted_batch_rejects_ambiguous_or_unknown_input(
    -    tmp_path: Path,
    -    payload: dict[str, object],
    -    message: str,
    -) -> None:
    -    batch_path = tmp_path / "trusted.json"
    -    batch_path.write_text(json.dumps(payload), encoding="utf-8")
    -
    -    with pytest.raises(ValueError, match=message):
    -        load_trusted_plugin_batch(batch_path)
    -
    -
    -def test_maintenance_lock_fences_both_runtime_owners(tmp_path: Path) -> None:
    -    maintenance = WorkspaceMaintenanceLock(tmp_path)
    -    supervisor = _SupervisorLock(tmp_path)
    -    supervisor.acquire()
    -    with pytest.raises(RuntimeError, match="生命周期 owner"):
    -        maintenance.acquire()
    -    supervisor.release()
    -
    -    runtime = WorkspaceInstanceLock(tmp_path)
    -    runtime.acquire()
    -    with pytest.raises(RuntimeError, match="生命周期 owner"):
    -        maintenance.acquire()
    -    runtime.release()
    -
    -    maintenance.acquire()
    -    maintenance.release()
    -
    -
    -@pytest.mark.asyncio
    -async def test_core_runtime_holds_shared_plugin_home_publication_lock(
    -    tmp_path: Path,
    -) -> None:
    -    home = tmp_path / "shared-plugin-home"
    -    observed: list[str] = []
    -
    -    async def noop() -> None:
    -        return None
    -
    -    class PluginManager:
    -        loaded_count = 0
    -
    -        async def load_all(self) -> None:
    -            competitor = PluginPublicationLock(home)
    -            with pytest.raises(RuntimeError, match="发布或消费 owner"):
    -                competitor.acquire()
    -            observed.append("publication-fenced")
    -
    -        async def terminate_all(self) -> None:
    -            observed.append("plugins-stopped")
    -
    -    runtime = object.__new__(CoreRuntime)
    -    runtime.plugin_manager = PluginManager()  # type: ignore[assignment]
    -    runtime.workspace = None
    -    runtime.plugin_publication_lock = PluginPublicationLock(home)
    -    runtime._plugin_publication_locked = False
    -    runtime.tools = SimpleNamespace(get_tool=lambda _name: None)
    -    runtime.loop = SimpleNamespace(shutdown_compaction=noop)
    -    runtime.event_bus = SimpleNamespace(aclose=noop)
    -    runtime.session_manager = SimpleNamespace(close=lambda: None)
    -
    -    await runtime.start()
    -
    -    assert observed == ["publication-fenced"]
    -    await runtime.stop()
    -    assert observed == ["publication-fenced", "plugins-stopped"]
    -    after_shutdown = PluginPublicationLock(home)
    -    after_shutdown.acquire()
    -    after_shutdown.release()
    -
    -
    -@pytest.mark.asyncio
    -async def test_inspect_modules_holds_plugin_publication_lock(tmp_path: Path) -> None:
    -    home = tmp_path / "shared-plugin-home"
    -
    -    async def load_and_stop() -> None:
    -        competitor = PluginPublicationLock(home)
    -        with pytest.raises(RuntimeError, match="发布或消费 owner"):
    -            competitor.acquire()
    -        raise RuntimeError("stop after inspect lock proof")
    -
    -    async def noop() -> None:
    -        return None
    -
    -    runtime = object.__new__(CoreRuntime)
    -    runtime.plugin_manager = SimpleNamespace(
    -        load_all=load_and_stop,
    -        terminate_all=noop,
    -    )
    -    runtime.plugin_publication_lock = PluginPublicationLock(home)
    -    runtime._plugin_publication_locked = False
    -    runtime.tools = SimpleNamespace(get_tool=lambda _name: None)
    -    runtime.loop = SimpleNamespace(shutdown_compaction=noop)
    -    runtime.event_bus = SimpleNamespace(aclose=noop)
    -    runtime.session_manager = SimpleNamespace(close=lambda: None)
    -
    -    with pytest.raises(RuntimeError, match="inspect lock proof"):
    -        await runtime.inspect_modules()
    -    await runtime.stop()
    -
    -    after_shutdown = PluginPublicationLock(home)
    -    after_shutdown.acquire()
    -    after_shutdown.release()
    -
    -
    -def test_trusted_batch_command_rejects_active_turn(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_PLUGIN_ROLLOUT_OWNER_TURN", "turn:owner")
    -    monkeypatch.setattr(
    -        main.sys,
    -        "argv",
    -        [
    -            "main.py",
    -            "plugin-install-trusted-batch",
    -            "--workspace",
    -            str(tmp_path / "workspace"),
    -            "--batch",
    -            str(tmp_path / "batch.json"),
    -            "--confirm-trusted",
    -        ],
    -    )
    -
    -    with pytest.raises(SystemExit, match="不能由 active turn 调用"):
    -        main._run_lightweight_command()
    -
    -
    -def test_trusted_batch_command_rejects_shared_home_used_by_other_workspace(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    home = tmp_path / "shared-plugin-home"
    -    owner = PluginPublicationLock(home)
    -    owner.acquire()
    -    monkeypatch.delenv("AKASHIC_PLUGIN_ROLLOUT_OWNER_TURN", raising=False)
    -    monkeypatch.setattr(
    -        main.sys,
    -        "argv",
    -        [
    -            "main.py",
    -            "plugin-install-trusted-batch",
    -            "--workspace",
    -            str(tmp_path / "idle-workspace"),
    -            "--plugins-home",
    -            str(home),
    -            "--batch",
    -            str(tmp_path / "missing.json"),
    -            "--confirm-trusted",
    -        ],
    -    )
    -
    -    try:
    -        with pytest.raises(SystemExit, match="发布或消费 owner"):
    -            main._run_lightweight_command()
    -    finally:
    -        owner.release()
    -
    -    workspace_owner = WorkspaceInstanceLock(tmp_path / "idle-workspace")
    -    workspace_owner.acquire()
    -    workspace_owner.release()
    -
    -
    -def test_trusted_batch_reports_completed_plugins_before_v2_failure(
    -    tmp_path: Path,
    -) -> None:
    -    accepted = _create_plugin_repository(tmp_path / "citation", "citation")
    -    rejected = _create_plugin_repository(
    -        tmp_path / "legacy",
    -        "legacy",
    -        api_version=2,
    -    )
    -    batch_path = tmp_path / "trusted.json"
    -    batch_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": str(repository),
    -                        "marketplace": "lab",
    -                        "ref": _git_output(repository, "rev-parse", "HEAD"),
    -                    }
    -                    for repository in (accepted, rejected)
    -                ],
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -    home = tmp_path / "plugins-home"
    -
    -    with pytest.raises(
    -        RuntimeError,
    -        match=r"index=1 completed=\['citation@lab'\].*api_version = 3",
    -    ):
    -        install_trusted_plugin_batch(
    -            workspace=tmp_path / "workspace",
    -            batch_path=batch_path,
    -            plugins_home=home,
    -        )
    -
    -    accepted_pointers = read_pointers(home / "cache" / "lab" / "citation")
    -    assert accepted_pointers is not None
    -    assert accepted_pointers.stable == accepted_pointers.latest
    -    assert not (home / "cache" / "lab" / "legacy" / ".pointers.json").exists()
    -
    -
    -def test_trusted_batch_command_prints_machine_readable_receipt(tmp_path: Path) -> None:
    -    repository = _create_plugin_repository(tmp_path / "citation", "citation")
    -    batch_path = tmp_path / "trusted.json"
    -    revision = _git_output(repository, "rev-parse", "HEAD")
    -    batch_path.write_text(
    -        json.dumps(
    -            {
    -                "schema_version": 1,
    -                "plugins": [
    -                    {
    -                        "source": str(repository),
    -                        "marketplace": "lab",
    -                        "ref": revision,
    -                    }
    -                ],
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    result = subprocess.run(
    -        [
    -            sys.executable,
    -            str(Path(main.__file__)),
    -            "plugin-install-trusted-batch",
    -            "--workspace",
    -            str(tmp_path / "workspace"),
    -            "--plugins-home",
    -            str(tmp_path / "plugins-home"),
    -            "--batch",
    -            str(batch_path),
    -            "--confirm-trusted",
    -            "--json",
    -        ],
    -        cwd=tmp_path,
    -        capture_output=True,
    -        text=True,
    -        env={
    -            key: value
    -            for key, value in os.environ.items()
    -            if key != "AKASHIC_PLUGIN_ROLLOUT_OWNER_TURN"
    -        },
    -        check=False,
    -    )
    -
    -    assert result.returncode == 0, result.stderr
    -    receipt = json.loads(result.stdout)
    -    assert receipt["programmaticValidation"] == "bypassed_by_operator_trust"
    -    assert receipt["plugins"][0]["sourceRevision"] == revision
    -
    -
    -def _create_plugin_repository(
    -    path: Path,
    -    name: str,
    -    *,
    -    api_version: int = 3,
    -    python_runtime: bool = False,
    -) -> Path:
    -    path.mkdir(parents=True)
    -    (path / "plugin.py").write_text(
    -        f"api_version = {api_version}\nname = {name!r}\nversion = '1.0.0'\n",
    -        encoding="utf-8",
    -    )
    -    python_declaration = ""
    -    if python_runtime:
    -        (path / "mcp").mkdir()
    -        (path / "mcp/requirements.txt").write_text("", encoding="utf-8")
    -        python_declaration = (
    -            "\n[[python]]\n"
    -            "requirements = 'mcp/requirements.txt'\n"
    -        )
    -    (path / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        f"name = {name!r}\n"
    -        "version = '1.0.0'\n"
    -        f"api_version = {api_version}\n"
    -        "entrypoint = 'plugin.py'\n"
    -        + python_declaration,
    -        encoding="utf-8",
    -    )
    -    for args in (
    -        ("init",),
    -        ("config", "user.name", "test"),
    -        ("config", "user.email", "test@example.com"),
    -        ("add", "."),
    -        ("commit", "-m", "init"),
    -    ):
    -        result = subprocess.run(
    -            ["git", *args],
    -            cwd=path,
    -            capture_output=True,
    -            text=True,
    -            env=os.environ.copy(),
    -            check=False,
    -        )
    -        assert result.returncode == 0, result.stderr
    -    return path
    -
    -
    -def _git_output(repository: Path, *args: str) -> str:
    -    result = subprocess.run(
    -        ["git", *args],
    -        cwd=repository,
    -        capture_output=True,
    -        text=True,
    -        env=os.environ.copy(),
    -        check=False,
    -    )
    -    assert result.returncode == 0, result.stderr
    -    return result.stdout.strip()
    diff --git a/tests/test_plugin_v3_only_surface.py b/tests/test_plugin_v3_only_surface.py
    deleted file mode 100644
    index 0f3a683b5..000000000
    --- a/tests/test_plugin_v3_only_surface.py
    +++ /dev/null
    @@ -1,54 +0,0 @@
    -from __future__ import annotations
    -
    -import inspect
    -from pathlib import Path
    -from types import ModuleType
    -
    -import pytest
    -
    -from agent.lifecycle.phases.after_reasoning import default_after_reasoning_modules
    -from agent.lifecycle.phases.after_step import default_after_step_modules
    -from agent.lifecycle.phases.after_turn import default_after_turn_modules
    -from agent.lifecycle.phases.before_reasoning import default_before_reasoning_modules
    -from agent.lifecycle.phases.before_step import default_before_step_modules
    -from agent.lifecycle.phases.before_turn import default_before_turn_modules
    -from agent.lifecycle.phases.prompt_render import default_prompt_render_modules
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.manifest import load_plugin_manifest
    -
    -
    -@pytest.mark.parametrize(
    -    "factory",
    -    (
    -        default_before_turn_modules,
    -        default_before_reasoning_modules,
    -        default_prompt_render_modules,
    -        default_before_step_modules,
    -        default_after_step_modules,
    -        default_after_reasoning_modules,
    -        default_after_turn_modules,
    -    ),
    -)
    -def test_core_phase_factories_have_no_plugin_module_injection(factory) -> None:
    -    assert "plugin_modules" not in inspect.signature(factory).parameters
    -
    -
    -def test_plugin_loader_rejects_v2_module() -> None:
    -    module = ModuleType("removed_api")
    -    module.api_version = 2
    -    module.name = "removed-api"
    -    module.version = "1.0.0"
    -    module.apply = lambda ctx, config: None
    -
    -    with pytest.raises(ValueError, match="api_version = 3"):
    -        ComposablePlugin.from_module(module)
    -
    -
    -def test_plugin_manifest_rejects_removed_package_shell(tmp_path: Path) -> None:
    -    (tmp_path / "manifest.toml").write_text(
    -        '[plugins]\n\n[packages."legacy"]\nenabled = true\n',
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(ValueError, match="不再支持 \\[packages\\]"):
    -        load_plugin_manifest(tmp_path)
    diff --git a/tests/test_plugin_workload_core.py b/tests/test_plugin_workload_core.py
    deleted file mode 100644
    index 9cc35d35c..000000000
    --- a/tests/test_plugin_workload_core.py
    +++ /dev/null
    @@ -1,513 +0,0 @@
    -from __future__ import annotations
    -
    -import sys
    -import threading
    -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugin_composition import (
    -    MCP_SERVERS,
    -    WORKLOADS,
    -    CompositionRoot,
    -    McpServerDefinition,
    -    PluginRuntime,
    -    Workload,
    -    WorkloadData,
    -    WorkloadEnv,
    -    WorkloadHealth,
    -    WorkloadLimits,
    -    WorkloadPort,
    -)
    -from agent.plugin_composition.mcp_slots import PluginMcpServers
    -from agent.plugin_composition.workload_slots import (
    -    PluginWorkloads,
    -    _freeze_plugin_workloads,
    -)
    -from agent.plugin_composition.model import CompositionError
    -from agent.plugins.composition_generation_host import CompositionGenerationHost
    -from agent.plugins.generation import GateResult, PluginContributions, PluginGeneration
    -from agent.plugins.scope import PluginScope
    -from agent.plugins.snapshot import RuntimeSnapshotCompiler
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.workload_generation_host import WorkloadGenerationHost
    -from bus.event_bus import EventBus
    -from agent.tools.registry import ToolRegistry
    -from agent.workloads.model import (
    -    WorkloadEndpoint,
    -    WorkloadLease,
    -    WorkloadStartRequest,
    -    WorkloadStartReceipt,
    -    WorkloadStopReceipt,
    -)
    -from agent.workloads.client import WorkloadEffectUnknown
    -
    -_IMAGE = "example.invalid/worker@sha256:" + "a" * 64
    -
    -
    -class _ReadyHandler(BaseHTTPRequestHandler):
    -    def do_GET(self) -> None:
    -        self.send_response(200)
    -        self.end_headers()
    -
    -    def log_message(self, *_args: object) -> None:
    -        return
    -
    -
    -class _FakeController:
    -    def __init__(self, endpoint: str) -> None:
    -        self.endpoint = endpoint
    -        self.starts: list[WorkloadStartRequest] = []
    -        self.stops: list[WorkloadLease] = []
    -
    -    async def start(self, request: WorkloadStartRequest) -> WorkloadStartReceipt:
    -        self.starts.append(request)
    -        lease = WorkloadLease(
    -            workspace_id=request.workspace_id,
    -            plugin_id=request.plugin_id,
    -            workload=request.workload,
    -            mode=request.mode,
    -            transaction_id=request.transaction_id,
    -            generation_id=request.generation_id,
    -            container_id=f"container-{len(self.starts)}",
    -            spec_digest=request.spec_digest,
    -        )
    -        return WorkloadStartReceipt(
    -            lease,
    -            (WorkloadEndpoint("gateway", self.endpoint),),
    -            None,
    -        )
    -
    -    async def stop(self, lease: WorkloadLease) -> WorkloadStopReceipt:
    -        self.stops.append(lease)
    -        return WorkloadStopReceipt(lease, True, True)
    -
    -    async def cleanup_candidates(
    -        self, workspace_id: str
    -    ) -> tuple[WorkloadStopReceipt, ...]:
    -        _ = workspace_id
    -        return ()
    -
    -
    -class _StopOnceController(_FakeController):
    -    def __init__(self, endpoint: str, fail_name: str) -> None:
    -        super().__init__(endpoint)
    -        self.fail_name = fail_name
    -        self.failed = False
    -
    -    async def stop(self, lease: WorkloadLease) -> WorkloadStopReceipt:
    -        self.stops.append(lease)
    -        if lease.workload == self.fail_name and not self.failed:
    -            self.failed = True
    -            raise RuntimeError("temporary stop failure")
    -        return WorkloadStopReceipt(lease, True, True)
    -
    -
    -class _LostStartResponseController(_FakeController):
    -    def __init__(self, endpoint: str) -> None:
    -        super().__init__(endpoint)
    -        self.receipt: WorkloadStartReceipt | None = None
    -
    -    async def start(self, request: WorkloadStartRequest) -> WorkloadStartReceipt:
    -        if self.receipt is None:
    -            self.receipt = await super().start(request)
    -            raise WorkloadEffectUnknown("response lost")
    -        self.starts.append(request)
    -        return self.receipt
    -
    -
    -def _workload() -> Workload:
    -    return Workload(
    -        name="worker",
    -        image=_IMAGE,
    -        command=("serve",),
    -        ports=(WorkloadPort("gateway", 8080),),
    -        data=(WorkloadData("state", "/data"),),
    -        health=WorkloadHealth("gateway", "/health", 5.0),
    -        limits=WorkloadLimits(128, 1.0, 64),
    -    )
    -
    -
    -def _named_workload(name: str) -> Workload:
    -    workload = _workload()
    -    return Workload(
    -        name=name,
    -        image=workload.image,
    -        command=workload.command,
    -        ports=workload.ports,
    -        data=(WorkloadData(name, f"/{name}"),),
    -        health=workload.health,
    -        limits=workload.limits,
    -    )
    -
    -
    -def _runtime(plugin_dir: Path, data_dir: Path, workspace: Path) -> PluginRuntime:
    -    return PluginRuntime(
    -        plugin_id="fixture",
    -        generation_id="fixture:test",
    -        plugin_dir=plugin_dir,
    -        data_dir=data_dir,
    -        workspace=workspace,
    -        config=None,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_workload_registry_is_owner_scoped_and_fiber_owned(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("workload-root")
    -    declarations = PluginWorkloads(root.instance_token)
    -    await root.context.provide(WORKLOADS, declarations)
    -    plugin_dir = tmp_path / "fixture"
    -    data_dir = tmp_path / "data"
    -    workspace = tmp_path / "workspace"
    -    plugin_dir.mkdir()
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(WORKLOADS).register(ctx, _workload())
    -
    -    fiber = await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(WORKLOADS,),
    -        runtime=_runtime(plugin_dir, data_dir, workspace),
    -    )
    -    registry = _freeze_plugin_workloads(declarations, root.instance_token)
    -    binding = registry.owned("fixture", "worker")
    -    assert binding is not None
    -    assert binding.descriptor.image == _IMAGE
    -    assert binding.descriptor.user_namespaces is False
    -
    -    await fiber.dispose()
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_one_plugin_cannot_give_two_workloads_the_same_writable_data(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("workload-writer-owner")
    -    workloads = PluginWorkloads(root.instance_token)
    -    await root.context.provide(WORKLOADS, workloads)
    -    plugin_dir = tmp_path / "fixture"
    -    data_dir = tmp_path / "data"
    -    workspace = tmp_path / "workspace"
    -    plugin_dir.mkdir()
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(WORKLOADS).register(ctx, _workload())
    -        await ctx.require(WORKLOADS).register(
    -            ctx,
    -            Workload(
    -                name="other",
    -                image=_IMAGE,
    -                command=("serve",),
    -                ports=(WorkloadPort("gateway", 8081),),
    -                data=(WorkloadData("state", "/other"),),
    -                health=WorkloadHealth("gateway", "/health", 5.0),
    -                limits=WorkloadLimits(128, 1.0, 64),
    -            ),
    -        )
    -
    -    await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(WORKLOADS,),
    -        runtime=_runtime(plugin_dir, data_dir, workspace),
    -    )
    -    with pytest.raises(CompositionError, match="多个 writer"):
    -        _freeze_plugin_workloads(workloads, root.instance_token)
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_composition_host_injects_same_owner_workload_url_into_mcp(
    -    tmp_path: Path,
    -) -> None:
    -    server = ThreadingHTTPServer(("127.0.0.1", 0), _ReadyHandler)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    endpoint = f"http://127.0.0.1:{server.server_port}"
    -    controller = _FakeController(endpoint)
    -
    -    plugin_dir = tmp_path / "fixture"
    -    data_dir = tmp_path / "data"
    -    workspace = tmp_path / "workspace"
    -    plugin_dir.mkdir()
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -    mcp_script = plugin_dir / "mcp.py"
    -    mcp_script.write_text(
    -        "import json, os, sys\n"
    -        "for raw in sys.stdin:\n"
    -        " msg=json.loads(raw); method=msg.get('method')\n"
    -        " if method=='initialize': result={'protocolVersion':'2025-11-25'}\n"
    -        " elif method=='tools/list': result={'tools':[{'name':'where','inputSchema':{'type':'object'}}]}\n"
    -        " elif method=='tools/call': result={'content':[{'type':'text','text':os.environ['WORKER_URL']}]}\n"
    -        " else: continue\n"
    -        " print(json.dumps({'jsonrpc':'2.0','id':msg['id'],'result':result}),flush=True)\n",
    -        encoding="utf-8",
    -    )
    -    root = CompositionRoot("workload-composition")
    -    workloads = PluginWorkloads(root.instance_token)
    -    mcp = PluginMcpServers(root.instance_token)
    -    await root.context.provide(WORKLOADS, workloads)
    -    await root.context.provide(MCP_SERVERS, mcp)
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(WORKLOADS).register(ctx, _workload())
    -        await ctx.require(MCP_SERVERS).register(
    -            ctx,
    -            McpServerDefinition(
    -                name="fixture",
    -                command=("python", "mcp.py"),
    -                required_tools=("where",),
    -                candidate_read_only_tools=("where",),
    -                workload_env=(WorkloadEnv("WORKER_URL", "worker", "gateway"),),
    -            ),
    -        )
    -
    -    await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(WORKLOADS, MCP_SERVERS),
    -        runtime=_runtime(plugin_dir, data_dir, workspace),
    -    )
    -    generation = PluginGeneration(
    -        plugin_id="fixture",
    -        generation_id="fixture:test",
    -        module_path="plugins.fixture",
    -        source_revision="source",
    -        config_revision="config",
    -        plugin_dir=plugin_dir,
    -        data_dir=data_dir,
    -        config=None,
    -        instance=object(),
    -        scope=PluginScope("fixture"),
    -        contributions=PluginContributions(manifest={}),
    -        gate_result=GateResult(
    -            gate_id="gate",
    -            plugin_id="fixture",
    -            candidate_revision="source",
    -            status="passed",
    -            checks=(),
    -        ),
    -        static_runtime_commands=(("mcp:fixture", (sys.executable, str(mcp_script))),),
    -    )
    -    snapshot = RuntimeSnapshotCompiler().compile(
    -        {"fixture": generation},
    -        composition_root=root,
    -    )
    -    snapshot.tool_registry = ToolRegistry(follow_runtime_snapshot=False)
    -    host = CompositionGenerationHost(
    -        workload_controller=controller,
    -        workspace_id="workspace-test",
    -    )
    -    try:
    -        runtime = await host.start(generation, snapshot, mode="candidate")
    -        assert runtime is not None and runtime.workloads is not None
    -        registry = host.attach_tools(snapshot.tool_registry, runtime)
    -        assert registry is not None
    -        tool = registry.get_tool("mcp_fixture__where")
    -        assert tool is not None
    -        assert await tool.execute() == endpoint
    -        assert controller.starts[0].data == (("state", "/data", True),)
    -    finally:
    -        await host.stop(generation.generation_id)
    -        await root.dispose()
    -        server.shutdown()
    -        thread.join(timeout=5)
    -        server.server_close()
    -    assert len(controller.stops) == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_cleanup_retry_only_stops_entries_that_are_still_owned(
    -    tmp_path: Path,
    -) -> None:
    -    root = CompositionRoot("workload-cleanup-retry")
    -    workloads = PluginWorkloads(root.instance_token)
    -    await root.context.provide(WORKLOADS, workloads)
    -    plugin_dir = tmp_path / "fixture"
    -    data_dir = tmp_path / "data"
    -    workspace = tmp_path / "workspace"
    -    plugin_dir.mkdir()
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(WORKLOADS).register(ctx, _named_workload("first"))
    -        await ctx.require(WORKLOADS).register(ctx, _named_workload("second"))
    -
    -    await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(WORKLOADS,),
    -        runtime=_runtime(plugin_dir, data_dir, workspace),
    -    )
    -    registry = _freeze_plugin_workloads(workloads, root.instance_token)
    -    controller = _StopOnceController("http://127.0.0.1:1", "first")
    -    host = WorkloadGenerationHost(
    -        controller,
    -        workspace_id="workspace-test",
    -        health_probe=lambda _url, _timeout: _ready(),
    -    )
    -    bindings = {
    -        binding.descriptor.name: binding
    -        for binding in registry.values()
    -        if binding.descriptor.owner == "fixture"
    -    }
    -    await host.start_generation("fixture:test", "fixture", bindings, mode="formal")
    -
    -    with pytest.raises(BaseExceptionGroup):
    -        await host.stop_generation("fixture:test")
    -    assert [lease.workload for lease in controller.stops] == ["second", "first"]
    -
    -    await host.retry_generation_cleanup("fixture:test")
    -    assert [lease.workload for lease in controller.stops] == [
    -        "second",
    -        "first",
    -        "first",
    -    ]
    -    await root.dispose()
    -
    -
    -@pytest.mark.asyncio
    -async def test_lost_start_response_is_recovered_and_stopped(tmp_path: Path) -> None:
    -    root = CompositionRoot("workload-lost-response")
    -    workloads = PluginWorkloads(root.instance_token)
    -    await root.context.provide(WORKLOADS, workloads)
    -    plugin_dir = tmp_path / "fixture"
    -    data_dir = tmp_path / "data"
    -    workspace = tmp_path / "workspace"
    -    plugin_dir.mkdir()
    -    data_dir.mkdir()
    -    workspace.mkdir()
    -
    -    async def apply(ctx) -> None:
    -        await ctx.require(WORKLOADS).register(ctx, _workload())
    -
    -    await root.mount(
    -        apply,
    -        name="fixture",
    -        inject=(WORKLOADS,),
    -        runtime=_runtime(plugin_dir, data_dir, workspace),
    -    )
    -    registry = _freeze_plugin_workloads(workloads, root.instance_token)
    -    binding = registry.owned("fixture", "worker")
    -    assert binding is not None
    -    controller = _LostStartResponseController("http://127.0.0.1:1")
    -    host = WorkloadGenerationHost(
    -        controller,
    -        workspace_id="workspace-test",
    -        health_probe=lambda _url, _timeout: _ready(),
    -    )
    -
    -    with pytest.raises(WorkloadEffectUnknown):
    -        await host.start_generation(
    -            "fixture:test", "fixture", {"worker": binding}, mode="formal"
    -        )
    -
    -    assert len(controller.starts) == 2
    -    assert len(controller.stops) == 1
    -    assert host.get("fixture:test") is None
    -    await root.dispose()
    -
    -
    -async def _ready() -> tuple[bool, str]:
    -    return True, "ready"
    -
    -
    -def _write_external_fixture(plugin_dir: Path, version: str) -> None:
    -    plugin_dir.mkdir(parents=True, exist_ok=True)
    -    (plugin_dir / "plugin.py").write_text(
    -        "from agent.plugin_composition import (WORKLOADS, Workload, WorkloadData, "
    -        "WorkloadHealth, WorkloadLimits, WorkloadPort)\n"
    -        "api_version = 3\n"
    -        "name = 'outside-box'\n"
    -        f"version = {version!r}\n"
    -        "inject = (WORKLOADS,)\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.require(WORKLOADS).register(ctx, Workload(\n"
    -        "        name='worker',\n"
    -        f"        image={_IMAGE!r},\n"
    -        "        command=('serve',),\n"
    -        "        ports=(WorkloadPort('gateway', 8080),),\n"
    -        "        data=(WorkloadData('state', '/data'),),\n"
    -        "        health=WorkloadHealth('gateway', '/health', 5.0),\n"
    -        "        limits=WorkloadLimits(128, 1.0, 64),\n"
    -        "    ))\n",
    -        encoding="utf-8",
    -    )
    -    (plugin_dir / "akashic.plugin.toml").write_text(
    -        "schema_version = 1\n"
    -        "name = 'outside-box'\n"
    -        f"version = {version!r}\n"
    -        "api_version = 3\n"
    -        "entrypoint = 'plugin.py'\n\n"
    -        "[[workload]]\n"
    -        "name = 'worker'\n"
    -        f"image = {_IMAGE!r}\n"
    -        "command = ['serve']\n\n"
    -        "[[workload.ports]]\n"
    -        "name = 'gateway'\n"
    -        "number = 8080\n\n"
    -        "[[workload.data]]\n"
    -        "name = 'state'\n"
    -        "target = '/data'\n"
    -        "writable = true\n\n"
    -        "[workload.health]\n"
    -        "port = 'gateway'\n"
    -        "path = '/health'\n"
    -        "timeout_seconds = 5.0\n\n"
    -        "[workload.limits]\n"
    -        "memory_mb = 128\n"
    -        "cpu_count = 1.0\n"
    -        "pids = 64\n",
    -        encoding="utf-8",
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_external_fixture_updates_and_stops_through_public_workload_api(
    -    tmp_path: Path,
    -) -> None:
    -    server = ThreadingHTTPServer(("127.0.0.1", 0), _ReadyHandler)
    -    thread = threading.Thread(target=server.serve_forever, daemon=True)
    -    thread.start()
    -    controller = _FakeController(f"http://127.0.0.1:{server.server_port}")
    -    plugin_dir = tmp_path / "plugins" / "outside-box"
    -    _write_external_fixture(plugin_dir, "1.0.0")
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        workspace=tmp_path / "workspace",
    -        installed_cache_root=tmp_path / "plugin-home" / "cache",
    -        workload_controller=controller,
    -    )
    -    try:
    -        await manager.load_all()
    -        stable = manager.current_snapshot
    -        assert stable is not None and stable.workload_registry is not None
    -        assert stable.workload_registry.owned("outside-box", "worker") is not None
    -
    -        _write_external_fixture(plugin_dir, "1.0.1")
    -        candidate = await manager.prepare_candidate("outside-box")
    -        assert candidate is not None and candidate.runtime_snapshot is not None
    -        result = await manager.publish_prepared("outside-box")
    -        assert result["publication_state"] == "committed"
    -        assert any(request.mode == "candidate" for request in controller.starts)
    -        assert manager.current_snapshot is not stable
    -    finally:
    -        await manager.terminate_all()
    -        server.shutdown()
    -        thread.join(timeout=5)
    -        server.server_close()
    -    assert controller.stops
    -    assert len(controller.stops) == len(controller.starts)
    diff --git a/tests/test_plugin_workload_manifest.py b/tests/test_plugin_workload_manifest.py
    deleted file mode 100644
    index 940c016ae..000000000
    --- a/tests/test_plugin_workload_manifest.py
    +++ /dev/null
    @@ -1,155 +0,0 @@
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.plugins.static_manifest import load_static_plugin_manifest
    -
    -
    -def _source(*, workload_ref: str = "worker", digest: str | None = None) -> str:
    -    image_digest = digest or "a" * 64
    -    return f"""
    -schema_version = 1
    -name = "fixture"
    -version = "1.0.0"
    -api_version = 3
    -entrypoint = "plugin.py"
    -
    -[[workload]]
    -name = "worker"
    -image = "example.invalid/worker@sha256:{image_digest}"
    -command = ["serve"]
    -
    -[[workload.ports]]
    -name = "gateway"
    -number = 8080
    -
    -[[workload.data]]
    -name = "state"
    -target = "/data"
    -writable = true
    -
    -[workload.health]
    -port = "gateway"
    -path = "/health"
    -timeout_seconds = 30
    -
    -[workload.limits]
    -memory_mb = 128
    -cpu_count = 1.0
    -pids = 64
    -
    -[[mcp]]
    -name = "fixture"
    -command = ["mcp.py"]
    -required_tools = ["read"]
    -candidate_read_only_tools = ["read"]
    -
    -[[mcp.workload_env]]
    -env = "WORKER_URL"
    -workload = "{workload_ref}"
    -port = "gateway"
    -"""
    -
    -
    -def _plugin(tmp_path: Path, source: str) -> Path:
    -    root = tmp_path / "fixture"
    -    root.mkdir()
    -    (root / "plugin.py").write_text("", encoding="utf-8")
    -    (root / "mcp.py").write_text("", encoding="utf-8")
    -    (root / "akashic.plugin.toml").write_text(source, encoding="utf-8")
    -    return root
    -
    -
    -def test_static_workload_and_mcp_binding_enter_identity(tmp_path: Path) -> None:
    -    root = _plugin(tmp_path, _source())
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.workloads[0].ports == (("gateway", 8080),)
    -    assert manifest.workloads[0].data == (("state", "/data", True),)
    -    assert manifest.workloads[0].user_namespaces is False
    -    assert manifest.mcp_servers[0].workload_env == (
    -        ("WORKER_URL", "worker", "gateway"),
    -    )
    -    original = manifest.identity_digest
    -    path = root / "akashic.plugin.toml"
    -    path.write_text(
    -        _source().replace("memory_mb = 128", "memory_mb = 256"), encoding="utf-8"
    -    )
    -    assert load_static_plugin_manifest(root).identity_digest != original
    -
    -
    -@pytest.mark.parametrize(
    -    ("memory", "cpu", "pids", "expected"),
    -    [
    -        ("0", "1.0", "64", (0, 1.0, 64)),
    -        ("128", "0.0", "64", (128, 0.0, 64)),
    -        ("128", "1.0", "0", (128, 1.0, 0)),
    -        ("0", "0.0", "0", (0, 0.0, 0)),
    -    ],
    -)
    -def test_static_workload_limits_can_be_unlimited_independently(
    -    tmp_path: Path,
    -    memory: str,
    -    cpu: str,
    -    pids: str,
    -    expected: tuple[int, float, int],
    -) -> None:
    -    source = (
    -        _source()
    -        .replace("memory_mb = 128", f"memory_mb = {memory}")
    -        .replace("cpu_count = 1.0", f"cpu_count = {cpu}")
    -        .replace("pids = 64", f"pids = {pids}")
    -    )
    -    root = _plugin(tmp_path, source)
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.workloads[0].limits == expected
    -
    -
    -def test_static_workload_user_namespaces_enter_identity(tmp_path: Path) -> None:
    -    root = _plugin(tmp_path, _source())
    -    original = load_static_plugin_manifest(root).identity_digest
    -    path = root / "akashic.plugin.toml"
    -
    -    path.write_text(
    -        _source().replace(
    -            'command = ["serve"]',
    -            'command = ["serve"]\nuser_namespaces = true',
    -        ),
    -        encoding="utf-8",
    -    )
    -
    -    manifest = load_static_plugin_manifest(root)
    -    assert manifest.workloads[0].user_namespaces is True
    -    assert manifest.identity_digest != original
    -
    -
    -def test_static_workload_rejects_unpinned_image(tmp_path: Path) -> None:
    -    root = _plugin(tmp_path, _source(digest="latest"))
    -
    -    with pytest.raises(ValueError, match="sha256 digest"):
    -        load_static_plugin_manifest(root)
    -
    -
    -def test_static_workload_loopback_port_enters_identity(tmp_path: Path) -> None:
    -    source = _source().replace("number = 8080", "number = 8080\nloopback = 18080")
    -    root = _plugin(tmp_path, source)
    -
    -    manifest = load_static_plugin_manifest(root)
    -
    -    assert manifest.workloads[0].loopback_ports == (("gateway", 18080),)
    -    original = manifest.identity_digest
    -    (root / "akashic.plugin.toml").write_text(
    -        source.replace("loopback = 18080", "loopback = 18081"),
    -        encoding="utf-8",
    -    )
    -    assert load_static_plugin_manifest(root).identity_digest != original
    -
    -
    -def test_static_mcp_rejects_unknown_workload_port(tmp_path: Path) -> None:
    -    root = _plugin(tmp_path, _source(workload_ref="missing"))
    -
    -    with pytest.raises(ValueError, match="未声明的 Workload"):
    -        load_static_plugin_manifest(root)
    diff --git a/tests/test_pre_execution_interceptor.py b/tests/test_pre_execution_interceptor.py
    deleted file mode 100644
    index fd7c37004..000000000
    --- a/tests/test_pre_execution_interceptor.py
    +++ /dev/null
    @@ -1,95 +0,0 @@
    -import asyncio
    -from pathlib import Path
    -from typing import Any, cast
    -from unittest.mock import MagicMock
    -
    -from agent.looping.core import AgentLoop
    -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps, LLMConfig
    -from agent.context import ContextBuilder
    -from bus.queue import MessageBus
    -from agent.plugin_composition import LLMResponse, ToolCall
    -from agent.tools.base import Tool
    -from agent.tools.registry import ToolRegistry
    -from tests.memory_fakes import FakeMemoryEngine
    -from tests.provider_fakes import ProviderContextBudgetStub
    -from tests.compaction_fakes import run_test_agent_loop
    -
    -
    -class _DummyTool(Tool):
    -    def __init__(self, name: str = "web_fetch") -> None:
    -        self._name = name
    -        self.calls: list[dict] = []
    -
    -    @property
    -    def name(self) -> str:
    -        return self._name
    -
    -    @property
    -    def description(self) -> str:
    -        return "dummy tool"
    -
    -    @property
    -    def parameters(self) -> dict:
    -        return {
    -            "type": "object",
    -            "properties": {"url": {"type": "string"}},
    -            "required": ["url"],
    -        }
    -
    -    async def execute(self, **kwargs) -> str:
    -        self.calls.append(kwargs)
    -        return "fetched"
    -
    -
    -class _FakeProvider(ProviderContextBudgetStub):
    -    def __init__(self, responses: list[LLMResponse]) -> None:
    -        self._responses = list(responses)
    -        self.calls: list[dict] = []
    -
    -    async def chat(self, **kwargs):
    -        self.calls.append(kwargs)
    -        return self._responses.pop(0)
    -
    -
    -def _make_loop(
    -    tmp_path: Path,
    -    provider: _FakeProvider,
    -    tool: Tool,
    -) -> AgentLoop:
    -    tools = ToolRegistry()
    -    tools.register(tool)
    -    loop = AgentLoop(
    -        AgentLoopDeps(
    -            bus=MessageBus(),
    -            tools=tools,
    -            session_manager=MagicMock(),
    -            workspace=tmp_path,
    -            context=ContextBuilder(tmp_path),
    -        ),
    -        AgentLoopConfig(llm=LLMConfig(max_iterations=5)),
    -    )
    -    return loop
    -
    -
    -def test_tool_executes_without_procedure_interceptor(tmp_path: Path):
    -    tool = _DummyTool()
    -    provider = _FakeProvider(
    -        [
    -            LLMResponse(
    -                content="",
    -                tool_calls=[
    -                    ToolCall(
    -                        "c1", "web_fetch", {"url": "https://www.bilibili.com/video/BV1"}
    -                    )
    -                ],
    -            ),
    -            LLMResponse(content="done", tool_calls=[]),
    -        ]
    -    )
    -    loop = _make_loop(tmp_path, provider, tool)
    -
    -    asyncio.run(
    -        run_test_agent_loop(loop, provider, [{"role": "user", "content": "test"}])
    -    )
    -
    -    assert len(tool.calls) == 1
    diff --git a/tests/test_prepare_container_rehearsal.py b/tests/test_prepare_container_rehearsal.py
    deleted file mode 100644
    index 1fda5686a..000000000
    --- a/tests/test_prepare_container_rehearsal.py
    +++ /dev/null
    @@ -1,366 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import sqlite3
    -import tomllib
    -from contextlib import closing
    -from pathlib import Path
    -
    -import pytest
    -
    -from scripts.container_rehearsal import workspace_snapshot
    -from scripts.container_rehearsal.model import SnapshotDriftError
    -from scripts.container_rehearsal.policy import excluded_reason
    -from scripts.container_rehearsal.prepare import prepare_rehearsal
    -from scripts.container_rehearsal.sqlite_snapshot import verify_session_media_references
    -
    -
    -def _write_config(path: Path) -> None:
    -    path.write_text(
    -        """
    -[runtime]
    -workspace = "/formal/workspace"
    -
    -[llm]
    -registry = "workspace"
    -
    -[channels.chat]
    -enabled = false
    -
    -[channels.telegram]
    -enabled = true
    -token = "telegram-secret"
    -allow_from = ["owner"]
    -
    -[channels.qq]
    -enabled = true
    -bot_uin = "123456"
    -allow_from = ["owner"]
    -
    -[mobile_realtime]
    -enabled = true
    -public_url = "wss://mobile.example/ws"
    -""".lstrip(),
    -        encoding="utf-8",
    -    )
    -
    -
    -def _create_live_database(path: Path) -> sqlite3.Connection:
    -    path.parent.mkdir(parents=True, exist_ok=True)
    -    connection = sqlite3.connect(path)
    -    connection.execute("PRAGMA journal_mode = WAL")
    -    connection.execute("CREATE TABLE events (value TEXT NOT NULL)")
    -    connection.execute("CREATE TABLE messages (id TEXT PRIMARY KEY, extra TEXT)")
    -    connection.execute("INSERT INTO events VALUES ('live-row')")
    -    connection.commit()
    -    return connection
    -
    -
    -def test_prepare_rehearsal_copies_business_state_and_live_sqlite(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "formal-workspace"
    -    workspace.mkdir()
    -    (workspace / "memory").mkdir()
    -    (workspace / "memory" / "MEMORY.md").write_text("kept\n", encoding="utf-8")
    -    schedules = [{"id": "formal-job", "enabled": True, "channel": "mobile"}]
    -    (workspace / "schedules.json").write_text(
    -        json.dumps(schedules, ensure_ascii=False) + "\n", encoding="utf-8"
    -    )
    -    (workspace / "plugin-data" / "feed-github").mkdir(parents=True)
    -    (workspace / "plugin-data" / "feed-github" / "state.json").write_text(
    -        '{"kept": true}\n', encoding="utf-8"
    -    )
    -    (workspace / "uploads").mkdir()
    -    media = workspace / "uploads" / "photo.png"
    -    media.write_bytes(b"stable-photo")
    -    database = _create_live_database(workspace / "sessions.db")
    -    database.execute(
    -        "INSERT INTO messages VALUES (?, ?)",
    -        ("message-1", json.dumps({"media": [str(media)]})),
    -    )
    -    database.commit()
    -
    -    for excluded in ("backups", "cache", "downloads", "runtime", "rebuilds"):
    -        directory = workspace / excluded
    -        directory.mkdir()
    -        (directory / "must-not-copy.txt").write_text("excluded", encoding="utf-8")
    -    (workspace / ".runtime-ready.json").write_text("{}", encoding="utf-8")
    -    (workspace / "observe.db.corrupt.20260412-165929").write_bytes(
    -        b"SQLite format 3\x00broken"
    -    )
    -    (workspace / "skills").mkdir()
    -    (workspace / "skills" / "cached-skill").symlink_to(
    -        tmp_path / "plugin-cache" / "skill", target_is_directory=True
    -    )
    -    (workspace / "skills" / "local-skill").mkdir()
    -    (workspace / "skills" / "local-skill" / "SKILL.md").write_text(
    -        "local\n", encoding="utf-8"
    -    )
    -
    -    config = tmp_path / "config.toml"
    -    _write_config(config)
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    (plugin_home / "manifest.toml").write_text(
    -        "[plugins.feed]\nenabled = true\n\n"
    -        '[plugins."feed@github"]\nenabled = true\n',
    -        encoding="utf-8",
    -    )
    -    (plugin_home / "cache").mkdir()
    -    (plugin_home / "cache" / "code.py").write_text("not copied\n", encoding="utf-8")
    -    target = tmp_path / "rehearsal"
    -
    -    try:
    -        manifest_path = prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=target,
    -        )
    -    finally:
    -        database.close()
    -
    -    assert manifest_path == target / "rehearsal-manifest.json"
    -    assert (target / "workspace" / "memory" / "MEMORY.md").read_text() == "kept\n"
    -    assert (
    -        target / "workspace" / "plugin-data" / "feed-github" / "state.json"
    -    ).is_file()
    -    assert (target / "workspace" / "skills" / "local-skill" / "SKILL.md").is_file()
    -    assert not (target / "workspace" / "skills" / "cached-skill").exists()
    -    assert not (target / "workspace" / "backups").exists()
    -    assert not (target / "workspace" / "observe.db.corrupt.20260412-165929").exists()
    -    assert not (target / "workspace" / "sessions.db-wal").exists()
    -    with closing(sqlite3.connect(target / "workspace" / "sessions.db")) as copied:
    -        assert copied.execute("SELECT value FROM events").fetchall() == [("live-row",)]
    -        assert copied.execute("PRAGMA integrity_check").fetchall() == [("ok",)]
    -
    -    candidate = tomllib.loads((target / "config.toml").read_text(encoding="utf-8"))
    -    assert candidate["runtime"]["workspace"] == str(target / "workspace")
    -    assert candidate["llm"] == {"registry": "workspace"}
    -    assert candidate["channels"]["chat"]["enabled"] is True
    -    assert candidate["channels"]["telegram"]["enabled"] is False
    -    assert candidate["channels"]["telegram"]["token"] == ""
    -    assert candidate["channels"]["qq"]["enabled"] is False
    -    assert candidate["channels"]["qq"]["bot_uin"] == ""
    -    assert candidate["mobile_realtime"]["enabled"] is False
    -    assert (target / "workspace" / "schedules.json").read_text() == "[]\n"
    -    assert (
    -        json.loads((target / "workspace" / "schedules.source.json").read_text())
    -        == schedules
    -    )
    -
    -    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    -    serialized = json.dumps(manifest, ensure_ascii=False)
    -    assert "telegram-secret" not in serialized
    -    assert manifest["candidate"]["plugin_cache_copied"] is False
    -    assert manifest["candidate"]["plugin_manifest_copied_unmodified"] is False
    -    assert manifest["candidate"]["schedules_disabled"] == 1
    -    assert manifest["candidate"]["source_schedules"] == (
    -        "workspace/schedules.source.json"
    -    )
    -    assert manifest["candidate"]["plugins_disabled_until_rebuilt"] == ["feed@github"]
    -    plugin_manifest = tomllib.loads(
    -        (target / "plugin-home" / "manifest.toml").read_text(encoding="utf-8")
    -    )
    -    assert plugin_manifest["plugins"]["feed"]["enabled"] is True
    -    assert plugin_manifest["plugins"]["feed@github"]["enabled"] is False
    -    assert any(
    -        item["path"] == "observe.db.corrupt.20260412-165929"
    -        and item["reason"] == "forensic_corrupt_artifact"
    -        for item in manifest["excluded"]
    -    )
    -    assert manifest["cleanup"]["exact_paths"] == [str(target)]
    -    assert len(manifest["databases"]) == 1
    -    database_evidence = manifest["databases"][0]
    -    assert database_evidence["path"] == "sessions.db"
    -    assert database_evidence["source_integrity_check"] == "ok"
    -    assert database_evidence["target_integrity_check"] == "ok"
    -    assert database_evidence["workspace_media_references"] == {
    -        "checked": 1,
    -        "preexisting_missing": [],
    -        "status": "ok",
    -    }
    -    assert manifest["consistency"] == {
    -        "attempts": 1,
    -        "drift_retries": [],
    -        "max_attempts": 3,
    -    }
    -
    -
    -def test_prepare_rehearsal_refuses_existing_or_overlapping_target(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    marker = workspace / "keep.txt"
    -    marker.write_text("formal", encoding="utf-8")
    -    config = tmp_path / "config.toml"
    -    _write_config(config)
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    (plugin_home / "manifest.toml").write_text("[plugins]\n", encoding="utf-8")
    -
    -    existing = tmp_path / "existing"
    -    existing.mkdir()
    -    with pytest.raises(FileExistsError, match="尚不存在"):
    -        prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=existing,
    -        )
    -    with pytest.raises(ValueError, match="Workspace.*内部"):
    -        prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=workspace / "candidate",
    -        )
    -    assert marker.read_text(encoding="utf-8") == "formal"
    -    assert not (workspace / "candidate").exists()
    -
    -
    -def test_prepare_rehearsal_rejects_included_external_symlink_atomically(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "plugin-data").mkdir()
    -    (workspace / "plugin-data" / "escape").symlink_to(tmp_path / "outside")
    -    config = tmp_path / "config.toml"
    -    _write_config(config)
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    (plugin_home / "manifest.toml").write_text("[plugins]\n", encoding="utf-8")
    -    target = tmp_path / "candidate"
    -
    -    with pytest.raises(ValueError, match="符号链接"):
    -        prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=target,
    -        )
    -
    -    assert not target.exists()
    -    assert list(tmp_path.glob(".candidate.preparing-*")) == []
    -
    -
    -def test_nested_drift_skill_projection_exclusion_is_scoped() -> None:
    -    assert excluded_reason(
    -        Path("opportunity-v1/drift/skills/self-improvement"),
    -        is_symlink=True,
    -    ) == "rebuildable_skill_projection"
    -    assert excluded_reason(
    -        Path("opportunity-v1/drift/skills/regular-file"),
    -        is_symlink=False,
    -    ) is None
    -    assert excluded_reason(
    -        Path("opportunity-v1/assets/escape"),
    -        is_symlink=True,
    -    ) is None
    -
    -
    -def test_file_created_during_database_backup_retries_whole_snapshot(
    -    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "uploads").mkdir()
    -    database = _create_live_database(workspace / "sessions.db")
    -    config = tmp_path / "config.toml"
    -    _write_config(config)
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    (plugin_home / "manifest.toml").write_text("[plugins]\n", encoding="utf-8")
    -    target = tmp_path / "candidate"
    -    original_copy_sqlite = workspace_snapshot.copy_sqlite
    -    calls = 0
    -
    -    def create_file_then_backup(source: Path, destination: Path) -> dict[str, object]:
    -        nonlocal calls
    -        calls += 1
    -        if calls == 1:
    -            (workspace / "uploads" / "arrived-during-db.png").write_bytes(b"new")
    -        return original_copy_sqlite(source, destination)
    -
    -    monkeypatch.setattr(workspace_snapshot, "copy_sqlite", create_file_then_backup)
    -    try:
    -        manifest_path = prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=target,
    -        )
    -    finally:
    -        database.close()
    -
    -    assert calls == 2
    -    assert (
    -        target / "workspace" / "uploads" / "arrived-during-db.png"
    -    ).read_bytes() == b"new"
    -    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    -    assert manifest["consistency"]["attempts"] == 2
    -    assert (
    -        "added=['uploads/arrived-during-db.png']"
    -        in manifest["consistency"]["drift_retries"][0]
    -    )
    -
    -
    -def test_missing_workspace_media_reference_is_reported_as_preexisting(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    database = _create_live_database(workspace / "sessions.db")
    -    missing = workspace / "uploads" / "missing.png"
    -    database.execute(
    -        "INSERT INTO messages VALUES (?, ?)",
    -        ("message-missing", json.dumps({"media": [str(missing)]})),
    -    )
    -    database.commit()
    -    config = tmp_path / "config.toml"
    -    _write_config(config)
    -    plugin_home = tmp_path / "plugin-home"
    -    plugin_home.mkdir()
    -    (plugin_home / "manifest.toml").write_text("[plugins]\n", encoding="utf-8")
    -    target = tmp_path / "candidate"
    -
    -    try:
    -        manifest_path = prepare_rehearsal(
    -            source_workspace=workspace,
    -            source_config=config,
    -            plugin_home=plugin_home,
    -            target=target,
    -        )
    -    finally:
    -        database.close()
    -
    -    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    -    database_evidence = manifest["databases"][0]
    -    assert database_evidence["workspace_media_references"] == {
    -        "checked": 0,
    -        "preexisting_missing": ["uploads/missing.png"],
    -        "status": "ok",
    -    }
    -
    -
    -def test_existing_workspace_media_missing_from_copy_fails_loud(tmp_path: Path) -> None:
    -    source = tmp_path / "source"
    -    destination = tmp_path / "destination"
    -    (source / "uploads").mkdir(parents=True)
    -    destination.mkdir()
    -    media = source / "uploads" / "photo.png"
    -    media.write_bytes(b"photo")
    -    database = _create_live_database(destination / "sessions.db")
    -    database.execute(
    -        "INSERT INTO messages VALUES (?, ?)",
    -        ("message-omitted", json.dumps({"media": [str(media)]})),
    -    )
    -    database.commit()
    -    database.close()
    -    records: list[dict[str, object]] = [{"path": "sessions.db"}]
    -
    -    with pytest.raises(SnapshotDriftError, match="媒体未进入副本"):
    -        verify_session_media_references(source, destination, records)
    diff --git a/tests/test_prepare_runtime_checkout.py b/tests/test_prepare_runtime_checkout.py
    deleted file mode 100644
    index 7cac229a1..000000000
    --- a/tests/test_prepare_runtime_checkout.py
    +++ /dev/null
    @@ -1,49 +0,0 @@
    -from __future__ import annotations
    -
    -import subprocess
    -from pathlib import Path
    -
    -from scripts.prepare_runtime_checkout import prepare_runtime_checkout
    -
    -
    -def _git(repository: Path, *arguments: str) -> str:
    -    return subprocess.run(
    -        ["git", *arguments],
    -        cwd=repository,
    -        check=True,
    -        capture_output=True,
    -        text=True,
    -    ).stdout.strip()
    -
    -
    -def test_runtime_checkout_does_not_carry_deleted_secret_history(tmp_path: Path) -> None:
    -    source = tmp_path / "source"
    -    source.mkdir()
    -    _git(source, "init", "-q")
    -    _git(source, "config", "user.email", "test@example.invalid")
    -    _git(source, "config", "user.name", "Test")
    -    secret = source / "config.toml"
    -    secret.write_text("token='secret'\n", encoding="utf-8")
    -    _git(source, "add", ".")
    -    _git(source, "commit", "-qm", "secret parent")
    -    parent = _git(source, "rev-parse", "HEAD")
    -    secret.unlink()
    -    (source / "main.py").write_text("print('safe')\n", encoding="utf-8")
    -    _git(source, "add", "-A")
    -    _git(source, "commit", "-qm", "safe release")
    -    commit = _git(source, "rev-parse", "HEAD")
    -
    -    target = prepare_runtime_checkout(
    -        source, commit, tmp_path / "runtime", "git@example.invalid:owner/repo.git"
    -    )
    -
    -    assert _git(target, "rev-parse", "HEAD") == commit
    -    assert _git(target, "rev-list", "--all", "--count") == "1"
    -    missing_parent = subprocess.run(
    -        ["git", "cat-file", "-e", parent], cwd=target, capture_output=True
    -    )
    -    assert missing_parent.returncode != 0
    -    assert not (target / "config.toml").exists()
    -    assert _git(target, "remote", "get-url", "origin") == (
    -        "git@example.invalid:owner/repo.git"
    -    )
    diff --git a/tests/test_presence.py b/tests/test_presence.py
    deleted file mode 100644
    index a0f147fb4..000000000
    --- a/tests/test_presence.py
    +++ /dev/null
    @@ -1,127 +0,0 @@
    -from contextlib import closing
    -from datetime import datetime, timezone
    -import sqlite3
    -
    -from session.activity import PresenceStore
    -from session.store import SessionStore
    -
    -
    -def _utc(year, month, day, hour=0, minute=0) -> datetime:
    -    return datetime(year, month, day, hour, minute, tzinfo=timezone.utc)
    -
    -
    -def _store(tmp_path) -> SessionStore:
    -    return SessionStore(tmp_path / "sessions.db")
    -
    -
    -def test_fresh_store_has_no_sessions(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    assert store.get_all_sessions() == {}
    -
    -
    -def test_record_user_message_stores_timestamp(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    t = _utc(2026, 2, 23, 10, 0)
    -    store.record_user_message("telegram:123", now=t)
    -    assert store.get_last_user_at("telegram:123") == t
    -
    -
    -def test_record_user_message_overwrites_previous(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    t1 = _utc(2026, 2, 20, 10, 0)
    -    t2 = _utc(2026, 2, 23, 10, 0)
    -    store.record_user_message("telegram:123", now=t1)
    -    store.record_user_message("telegram:123", now=t2)
    -    assert store.get_last_user_at("telegram:123") == t2
    -
    -
    -def test_record_proactive_sent_stores_timestamp(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    t = _utc(2026, 2, 22, 15, 0)
    -    store.record_proactive_sent("telegram:123", now=t)
    -    assert store.get_last_proactive_at("telegram:123") == t
    -
    -
    -def test_nonexistent_session_returns_none(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    assert store.get_last_user_at("no:session") is None
    -    assert store.get_last_proactive_at("no:session") is None
    -
    -
    -def test_most_recent_user_at_returns_latest_across_sessions(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    t_old = _utc(2026, 2, 20, 10, 0)
    -    t_new = _utc(2026, 2, 23, 8, 0)
    -    store.record_user_message("telegram:111", now=t_old)
    -    store.record_user_message("qq:222", now=t_new)
    -    assert store.most_recent_user_at() == t_new
    -
    -
    -def test_most_recent_user_at_is_none_when_no_sessions(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    assert store.most_recent_user_at() is None
    -
    -
    -def test_get_all_sessions_returns_both_sessions(tmp_path):
    -    store = PresenceStore(_store(tmp_path))
    -    t1 = _utc(2026, 2, 20, 10, 0)
    -    t2 = _utc(2026, 2, 23, 8, 0)
    -    store.record_user_message("telegram:111", now=t1)
    -    store.record_user_message("qq:222", now=t2)
    -    all_s = store.get_all_sessions()
    -    assert set(all_s.keys()) == {"telegram:111", "qq:222"}
    -    assert all_s["telegram:111"]["last_user_at"] == t1
    -    assert all_s["qq:222"]["last_user_at"] == t2
    -
    -
    -def test_persistence_survives_reload(tmp_path):
    -    db_path = tmp_path / "sessions.db"
    -    t_user = _utc(2026, 2, 23, 10, 0)
    -    t_pro = _utc(2026, 2, 22, 15, 0)
    -
    -    store = PresenceStore(SessionStore(db_path))
    -    store.record_user_message("telegram:123", now=t_user)
    -    store.record_proactive_sent("telegram:123", now=t_pro)
    -
    -    store2 = PresenceStore(SessionStore(db_path))
    -    assert store2.get_last_user_at("telegram:123") == t_user
    -    assert store2.get_last_proactive_at("telegram:123") == t_pro
    -
    -
    -def test_activity_updates_preserve_session_schema_and_other_columns(tmp_path):
    -    db_path = tmp_path / "sessions.db"
    -    session_store = SessionStore(db_path)
    -    session_store.create_session(
    -        key="telegram:123",
    -        metadata={"channel": "telegram", "chat_id": "123"},
    -    )
    -
    -    with closing(sqlite3.connect(db_path)) as conn:
    -        schema_before = conn.execute("PRAGMA table_info(sessions)").fetchall()
    -        row_before = conn.execute(
    -            "SELECT key, metadata, last_consolidated FROM sessions WHERE key = ?",
    -            ("telegram:123",),
    -        ).fetchone()
    -
    -    activity = PresenceStore(session_store)
    -    user_at = _utc(2026, 2, 23, 10, 0)
    -    proactive_at = _utc(2026, 2, 23, 11, 0)
    -    activity.record_user_message("telegram:123", now=user_at)
    -    activity.record_proactive_sent("telegram:123", now=proactive_at)
    -
    -    with closing(sqlite3.connect(db_path)) as conn:
    -        schema_after = conn.execute("PRAGMA table_info(sessions)").fetchall()
    -        row_after = conn.execute(
    -            """
    -            SELECT key, metadata, last_consolidated, last_user_at, last_proactive_at
    -            FROM sessions WHERE key = ?
    -            """,
    -            ("telegram:123",),
    -        ).fetchone()
    -
    -    assert schema_after == schema_before
    -    assert row_after == (
    -        *row_before,
    -        user_at.isoformat(),
    -        proactive_at.isoformat(),
    -    )
    diff --git a/tests/test_proactive_feedback_event.py b/tests/test_proactive_feedback_event.py
    deleted file mode 100644
    index fd1d3f7c1..000000000
    --- a/tests/test_proactive_feedback_event.py
    +++ /dev/null
    @@ -1,70 +0,0 @@
    -from __future__ import annotations
    -
    -from dataclasses import FrozenInstanceError
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.turn_events.proactive_feedback import (
    -    PROACTIVE_FEEDBACK_COMMITTED,
    -    PROACTIVE_FEEDBACK_PREVIEW_MAX_CHARS,
    -    ProactiveFeedbackCommitted,
    -)
    -
    -
    -def _event(**changes: object) -> ProactiveFeedbackCommitted:
    -    values: dict[str, object] = {
    -        "event_id": "proactive_feedback:1",
    -        "session_key": "telegram:1",
    -        "user_message_id": "user-1",
    -        "assistant_message_id": "assistant-1",
    -        "proactive_message_id": "proactive-1",
    -        "feedback_type": "topic_follow",
    -        "confidence": "high",
    -        "pa_score": 0.8,
    -        "pua_score": 0.9,
    -        "lag_seconds": 12,
    -        "candidate_count": 2,
    -        "matched_by": "recent_pua",
    -        "reason": "pua_high",
    -        "user_content_preview": "用户回应",
    -        "assistant_content_preview": "助手继续",
    -        "proactive_content_preview": "主动消息",
    -    }
    -    values.update(changes)
    -    return ProactiveFeedbackCommitted(**cast(Any, values))
    -
    -
    -def test_feedback_payload_is_frozen_and_preview_bounded() -> None:
    -    event = _event(
    -        user_content_preview="x" * PROACTIVE_FEEDBACK_PREVIEW_MAX_CHARS,
    -        pa_score=1,
    -    )
    -
    -    assert event.pa_score == 1.0
    -    assert event.user_content_preview == "x" * PROACTIVE_FEEDBACK_PREVIEW_MAX_CHARS
    -    assert PROACTIVE_FEEDBACK_COMMITTED.name == "proactive.feedback.committed"
    -    with pytest.raises(FrozenInstanceError):
    -        event.reason = "changed"  # type: ignore[misc]
    -
    -
    -@pytest.mark.parametrize(
    -    "changes",
    -    (
    -        {"event_id": ""},
    -        {"assistant_message_id": " assistant-1"},
    -        {"pa_score": float("nan")},
    -        {"pua_score": float("inf")},
    -        {"lag_seconds": -1},
    -        {"candidate_count": True},
    -        {
    -            "proactive_content_preview": "x"
    -            * (PROACTIVE_FEEDBACK_PREVIEW_MAX_CHARS + 1)
    -        },
    -    ),
    -)
    -def test_feedback_payload_rejects_unbounded_or_malformed_values(
    -    changes: dict[str, object],
    -) -> None:
    -    with pytest.raises((TypeError, ValueError)):
    -        _event(**changes)
    diff --git a/tests/test_proactive_island_handoff.py b/tests/test_proactive_island_handoff.py
    deleted file mode 100644
    index 593d21bda..000000000
    --- a/tests/test_proactive_island_handoff.py
    +++ /dev/null
    @@ -1,1104 +0,0 @@
    -"""Real SQLite fixtures for the proactive-island active-state handoff."""
    -
    -from __future__ import annotations
    -
    -import hashlib
    -import json
    -import sqlite3
    -import sys
    -from collections.abc import Mapping
    -from datetime import UTC, datetime
    -from pathlib import Path
    -from typing import cast
    -
    -import pytest
    -
    -import agent.migrations.proactive_island.cli as handoff_cli
    -from agent.migrations.proactive_island.cli import apply as apply_cli
    -from agent.migrations.proactive_island.cli import backup_sources
    -from agent.migrations.proactive_island.cli import plan as plan_cli
    -from agent.migrations.proactive_island.cli import retire as retire_cli
    -from agent.migrations.proactive_island.handoff import (
    -    AdapterPlan,
    -    HandoffAdapter,
    -    HandoffStatus,
    -    TargetReceipt,
    -    apply_handoff,
    -    preflight_handoff,
    -    receipt_digest,
    -)
    -from agent.migrations.proactive_island.history import LegacyProactiveHistory
    -from agent.migrations.proactive_island.inventory import (
    -    LegacyFact,
    -    LegacyFactKind,
    -    inventory_digest,
    -    inventory_workspace,
    -)
    -from plugins.eventmail.store import EventMailStore
    -from plugins.wake.legacy_rules import read_archived_rules
    -from agent.migrations.proactive_island.wake_rules import WakeRulesArchiveAdapter
    -from scripts.proactive_island_handoff import main as handoff_main
    -from tests.fixtures.legacy_wake_state import (
    -    create_legacy_wake_database,
    -    populate_continuity_table,
    -)
    -
    -
    -def _workspace_state(root: Path) -> tuple[tuple[str, str, str], ...]:
    -    """Capture every path and file digest for a zero-write boundary oracle."""
    -
    -    state = []
    -    for path in sorted(root.rglob("*")):
    -        relative = str(path.relative_to(root))
    -        if path.is_file():
    -            state.append(
    -                (relative, "file", hashlib.sha256(path.read_bytes()).hexdigest())
    -            )
    -        else:
    -            state.append((relative, "directory", ""))
    -    return tuple(state)
    -
    -
    -def _wake_db(
    -    workspace: Path,
    -    rows: list[tuple[object, ...]],
    -    acknowledgements: list[tuple[object, ...]] | None = None,
    -) -> Path:
    -    path = workspace / "wake_proactive.db"
    -    workspace.mkdir(parents=True, exist_ok=True)
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        CREATE TABLE reservoir_events(
    -            item_id TEXT PRIMARY KEY, kind TEXT NOT NULL, source_id TEXT NOT NULL,
    -            original_source_id TEXT NOT NULL, ack_source_id TEXT,
    -            source_event_id TEXT NOT NULL, published_at TEXT NOT NULL,
    -            first_seen_at TEXT NOT NULL, preprocess_score REAL NOT NULL,
    -            payload_json TEXT NOT NULL, embedding_json TEXT, status TEXT NOT NULL,
    -            consumed_at TEXT
    -        );
    -        CREATE TABLE pending_acknowledgements(
    -            source_id TEXT NOT NULL, source_event_id TEXT NOT NULL,
    -            item_id TEXT NOT NULL DEFAULT '', action TEXT NOT NULL DEFAULT 'consume',
    -            queued_at TEXT NOT NULL,
    -            PRIMARY KEY(source_id, source_event_id, item_id)
    -        );
    -        CREATE TABLE wake_runs(
    -            wake_id TEXT PRIMARY KEY, session_key TEXT NOT NULL, now_utc TEXT NOT NULL,
    -            scratchpad_json TEXT NOT NULL, investigations_json TEXT NOT NULL,
    -            final_message TEXT NOT NULL, cited_ids_json TEXT NOT NULL,
    -            display_event_map_json TEXT NOT NULL, source_refs_json TEXT NOT NULL,
    -            investigation_completed INTEGER NOT NULL DEFAULT 0,
    -            terminal_action TEXT
    -        );
    -        """)
    -    connection.executemany(
    -        "INSERT INTO reservoir_events VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", rows
    -    )
    -    connection.executemany(
    -        "INSERT INTO pending_acknowledgements VALUES(?,?,?,?,?)", acknowledgements or []
    -    )
    -    connection.commit()
    -    connection.close()
    -    return path
    -
    -
    -def _wake_row(
    -    index: int,
    -    *,
    -    source: str = "feed@github:subscriptions",
    -    status: str = "unread",
    -    kind: str = "content",
    -    event_id: str | None = None,
    -    payload: Mapping[str, object] | None = None,
    -) -> tuple[object, ...]:
    -    event = event_id or f"event-{index}"
    -    body = dict(payload or {"title": f"item {index}", "content": f"body {index}"})
    -    body.update({"event_id": event, "ack_server": source, "kind": kind})
    -    return (
    -        f"{source}:{event}",
    -        kind,
    -        source,
    -        "feed",
    -        source,
    -        event,
    -        f"2026-08-23T00:{index:02d}:00+00:00",
    -        f"2026-08-23T01:{index:02d}:00+00:00",
    -        0.5,
    -        json.dumps(body, sort_keys=True),
    -        None,
    -        status,
    -        None,
    -    )
    -
    -
    -def _provider_db(path: Path, rows: int) -> None:
    -    path.parent.mkdir(parents=True, exist_ok=True)
    -    connection = sqlite3.connect(path)
    -    connection.execute(
    -        "CREATE TABLE items(event_id TEXT PRIMARY KEY, content_hash TEXT NOT NULL)"
    -    )
    -    connection.executemany(
    -        "INSERT INTO items VALUES(?, ?)",
    -        [(f"event-{index}", f"revision-{index}") for index in range(rows)],
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -
    -def _proactive_db(workspace: Path) -> Path:
    -    """Create a reduced fixture with the exact formal proactive table set."""
    -
    -    path = workspace / "proactive.db"
    -    workspace.mkdir(parents=True, exist_ok=True)
    -    connection = sqlite3.connect(path)
    -    connection.executescript("""
    -        CREATE TABLE deliveries(
    -            session_key TEXT, delivery_key TEXT, sent_at TEXT,
    -            PRIMARY KEY(session_key, delivery_key)
    -        );
    -        CREATE TABLE session_state(
    -            session_key TEXT, key TEXT, value TEXT,
    -            PRIMARY KEY(session_key, key)
    -        );
    -        CREATE TABLE context_only_timestamps(
    -            id INTEGER PRIMARY KEY AUTOINCREMENT, session_key TEXT, ts TEXT
    -        );
    -        CREATE TABLE tick_log(
    -            id INTEGER PRIMARY KEY AUTOINCREMENT, tick_id TEXT, session_key TEXT,
    -            started_at TEXT, finished_at TEXT
    -        );
    -        CREATE TABLE tick_step_log(
    -            id INTEGER PRIMARY KEY AUTOINCREMENT, tick_id TEXT, step_index INTEGER,
    -            phase TEXT, tool_name TEXT
    -        );
    -        CREATE TABLE rejection_cooldown(item_id TEXT PRIMARY KEY, until_utc TEXT);
    -        CREATE TABLE seen_items(item_id TEXT PRIMARY KEY, seen_at TEXT);
    -        CREATE TABLE semantic_items(item_id TEXT PRIMARY KEY, embedding BLOB);
    -        CREATE TABLE kv_state(key TEXT PRIMARY KEY, value TEXT);
    -        INSERT INTO deliveries VALUES('wake:default', 'delivery:1', '2026-08-23');
    -        INSERT INTO deliveries VALUES('wake:default', 'delivery:2', '2026-08-23');
    -        INSERT INTO session_state VALUES('wake:default', 'last_tick', 'one');
    -        INSERT INTO context_only_timestamps(session_key, ts)
    -            VALUES('wake:default', '2026-08-23');
    -        INSERT INTO tick_log(tick_id, session_key, started_at, finished_at)
    -            VALUES('tick:1', 'wake:default', '2026-08-23', '2026-08-23');
    -        INSERT INTO tick_step_log(tick_id, step_index, phase, tool_name)
    -            VALUES('tick:1', 0, 'content', 'poll');
    -        INSERT INTO rejection_cooldown VALUES('old:1', '2026-08-24');
    -        INSERT INTO seen_items VALUES('old:2', '2026-08-23');
    -        INSERT INTO semantic_items VALUES('old:3', X'0001');
    -        INSERT INTO kv_state VALUES('cursor', 'three');
    -        """)
    -    connection.commit()
    -    connection.close()
    -    return path
    -
    -
    -def _populate_wake_continuity(workspace: Path, table: str) -> Path:
    -    """Populate the frozen legacy schema with one continuity fact."""
    -
    -    path = workspace / "wake_proactive.db"
    -    create_legacy_wake_database(path)
    -    populate_continuity_table(path, table)
    -    return path
    -
    -
    -class _SourceAdapter(HandoffAdapter):
    -    """Simulate only a source-owned provider join around the real Content store."""
    -
    -    def __init__(self, provider: Path, content: EventMailStore) -> None:
    -        self.provider = provider
    -        self.content = content
    -        self.source_id = "feed-subscriptions"
    -        self.apply_calls = 0
    -
    -    def accepts(self, fact: LegacyFact) -> bool:
    -        return (
    -            fact.kind is LegacyFactKind.WAKE_SOURCE_ITEM
    -            and fact.source_identity == "feed@github:subscriptions"
    -        )
    -
    -    def plan(self, fact: LegacyFact) -> AdapterPlan:
    -        row = self._row(fact)
    -        event_id = cast(str, row["source_event_id"])
    -        connection = sqlite3.connect(
    -            self.provider.resolve().as_uri() + "?mode=ro", uri=True
    -        )
    -        result = connection.execute(
    -            "SELECT content_hash FROM items WHERE event_id=?", (event_id,)
    -        ).fetchone()
    -        connection.close()
    -        if result is None:
    -            raise RuntimeError("provider revision missing")
    -        return AdapterPlan(f"content:{self.source_id}:{event_id}:{str(result[0])}")
    -
    -    def apply(self, fact: LegacyFact, plan: AdapterPlan) -> TargetReceipt:
    -        self.apply_calls += 1
    -        _, _, event_id, revision = plan.target_identity.split(":", 3)
    -        row = self._row(fact)
    -        payload = json.loads(cast(str, row["payload_json"]))
    -        batch_id = f"legacy-wake:{event_id}:{revision}"
    -        receipt = self.content.submit(
    -            self.source_id,
    -            batch_id,
    -            (
    -                {
    -                    "item_id": event_id,
    -                    "revision": revision,
    -                    "payload": payload,
    -                    "not_before": row["published_at"],
    -                    "requires_ack": True,
    -                },
    -            ),
    -        )
    -        normalized = {"target_identity": plan.target_identity, "receipt": receipt}
    -        return TargetReceipt(
    -            receipt_id=cast(str, receipt["receipt_id"]),
    -            receipt_digest=receipt_digest(normalized),
    -            target_identity=plan.target_identity,
    -        )
    -
    -    def verify(self, fact: LegacyFact, receipt: TargetReceipt) -> bool:
    -        plan = self.plan(fact)
    -        if receipt.target_identity != plan.target_identity:
    -            return False
    -        _, _, event_id, revision = plan.target_identity.split(":", 3)
    -        batch_id = f"legacy-wake:{event_id}:{revision}"
    -        submission = self.content.read_submission(self.source_id, batch_id)
    -        item = self.content.read_revision(self.source_id, event_id, revision)
    -        if submission is None or item is None:
    -            return False
    -        normalized = {"target_identity": plan.target_identity, "receipt": submission}
    -        return receipt.receipt_id == submission[
    -            "receipt_id"
    -        ] and receipt.receipt_digest == receipt_digest(normalized)
    -
    -    @staticmethod
    -    def _row(fact: LegacyFact) -> dict[str, object]:
    -        value = json.loads(fact.opaque)
    -        assert isinstance(value, dict)
    -        return cast(dict[str, object], value)
    -
    -
    -def test_empty_plan_and_apply_write_nothing(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    before = tuple(workspace.rglob("*"))
    -
    -    assert plan_cli(workspace).status is HandoffStatus.READY
    -    report = apply_cli(workspace.resolve(), tmp_path / "unused-backup")
    -
    -    assert report.status is HandoffStatus.READY
    -    assert before == ()
    -    assert tuple(workspace.rglob("*")) == before
    -    assert not (tmp_path / "unused-backup").exists()
    -
    -
    -def test_active_source_plan_does_not_mount_or_initialize_content(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(1)])
    -    provider = tmp_path / "feed.sqlite3"
    -    _provider_db(provider, 2)
    -    target = workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3"
    -    adapter = _SourceAdapter(provider, EventMailStore(target))
    -    before = _workspace_state(workspace)
    -
    -    report = preflight_handoff(workspace, inventory_workspace(workspace), (adapter,))
    -
    -    assert report.status is HandoffStatus.PLAN
    -    assert _workspace_state(workspace) == before
    -    assert not target.exists()
    -
    -
    -def test_null_ack_source_uses_exact_reservoir_source_owner(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    row = list(_wake_row(1, source="feed@github:subscriptions"))
    -    row[4] = None
    -    _wake_db(workspace, [tuple(row)])
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    assert inventory.blocks == ()
    -    assert inventory.facts[0].source_identity == "feed@github:subscriptions"
    -
    -
    -def test_conflicting_wake_source_identity_blocks_with_exact_row_digest(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    first = _wake_row(1, event_id="same-event")
    -    second = list(_wake_row(2, event_id="same-event"))
    -    second[0] = "zz-another-item-id"
    -    _wake_db(workspace, [first, tuple(second)])
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    conflict = next(
    -        block
    -        for block in inventory.blocks
    -        if block.reason == "source_identity_conflict"
    -    )
    -    assert conflict.locator == "wake:reservoir_events:zz-another-item-id"
    -    assert len(conflict.source_digest) == 64
    -
    -
    -def test_unknown_proactive_table_blocks_without_copying_rows(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    path = workspace / "proactive.db"
    -    connection = sqlite3.connect(path)
    -    connection.execute("CREATE TABLE future_state(id TEXT, payload TEXT)")
    -    connection.execute("INSERT INTO future_state VALUES('one', 'opaque')")
    -    connection.commit()
    -    connection.close()
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    assert len(inventory.blocks) == 1
    -    assert inventory.blocks[0].locator == "proactive:future_state"
    -    assert inventory.blocks[0].reason == "unknown_proactive_table"
    -    assert len(inventory.blocks[0].source_digest) == 64
    -
    -
    -def test_duplicate_target_owners_block_without_calling_plan(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(1)])
    -    provider = tmp_path / "feed.sqlite3"
    -    _provider_db(provider, 2)
    -    adapter = _SourceAdapter(provider, EventMailStore(tmp_path / "unused.sqlite3"))
    -
    -    report = preflight_handoff(
    -        workspace, inventory_workspace(workspace), (adapter, adapter)
    -    )
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert report.items[0].reason == "owner_adapter_conflict"
    -    assert not (tmp_path / "unused.sqlite3").exists()
    -
    -
    -def test_formal_shape_inventory_keeps_generic_job_and_terminal_drift_historical(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(index) for index in range(15)])
    -    rules = b"x" * 7474
    -    (workspace / "PROACTIVE_CONTEXT.md").write_bytes(rules)
    -    drift = workspace / "drift" / "drift.db"
    -    drift.parent.mkdir()
    -    connection = sqlite3.connect(drift)
    -    connection.executescript("""
    -        CREATE TABLE skill_continuum(skill_name TEXT, last_status TEXT);
    -        CREATE TABLE runs(
    -            id INTEGER PRIMARY KEY, event_id TEXT, run_at TEXT, skill_name TEXT,
    -            status TEXT, briefing TEXT, message_result TEXT
    -        );
    -        INSERT INTO skill_continuum VALUES('one', 'completed');
    -        """)
    -    connection.executemany(
    -        "INSERT INTO runs VALUES(?,?,?,?,?,?,?)",
    -        [
    -            (index, f"drift-{index}", "2026-08-23", "one", "paused", "done", "silent")
    -            for index in range(13)
    -        ],
    -    )
    -    connection.commit()
    -    connection.close()
    -    jobs = workspace / "runtime" / "plugin-jobs" / "outcomes.sqlite"
    -    jobs.parent.mkdir(parents=True)
    -    connection = sqlite3.connect(jobs)
    -    connection.execute(
    -        "CREATE TABLE job_outcomes(plugin_id TEXT, job_name TEXT, invocation_id TEXT, "
    -        "state TEXT, created_at TEXT, event_payload_json TEXT)"
    -    )
    -    connection.executemany(
    -        "INSERT INTO job_outcomes VALUES(?,?,?,?,?,?)",
    -        [
    -            ("github-watch", "poll", "running", "running", "2026-08-23", None),
    -            (
    -                "emotion",
    -                "merge_proactive_pending",
    -                "done",
    -                "succeeded",
    -                "2026-08-22",
    -                "{}",
    -            ),
    -        ],
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    assert len(inventory.facts) == 16
    -    assert inventory.blocks == ()
    -    assert {
    -        fact.source_identity
    -        for fact in inventory.facts
    -        if fact.kind is LegacyFactKind.WAKE_SOURCE_ITEM
    -    } == {"feed@github:subscriptions"}
    -    rules_fact = next(
    -        fact for fact in inventory.facts if fact.kind is LegacyFactKind.WAKE_RULES
    -    )
    -    assert rules_fact.source_digest == hashlib.sha256(rules).hexdigest()
    -    history = LegacyProactiveHistory(workspace)
    -    assert len(history.drift_runs()) == 13
    -    assert {row["invocation_id"] for row in history.job_outcomes()} == {
    -        "running",
    -        "done",
    -    }
    -
    -
    -def test_proactive_continuity_blocks_once_per_table_and_history_stays_readable(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    _proactive_db(workspace)
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    assert {block.locator for block in inventory.blocks} == {
    -        "proactive:deliveries",
    -        "proactive:session_state",
    -        "proactive:context_only_timestamps",
    -        "proactive:rejection_cooldown",
    -        "proactive:seen_items",
    -        "proactive:kv_state",
    -    }
    -    deliveries = next(
    -        block for block in inventory.blocks if block.locator == "proactive:deliveries"
    -    )
    -    assert deliveries.reason == "proactive_continuity_owner_unavailable"
    -    assert deliveries.source_digest.startswith("rows=2;sha256=")
    -    history = LegacyProactiveHistory(workspace).proactive_tables()
    -    assert set(history) == {
    -        "deliveries",
    -        "session_state",
    -        "context_only_timestamps",
    -        "tick_log",
    -        "tick_step_log",
    -        "rejection_cooldown",
    -        "seen_items",
    -        "semantic_items",
    -        "kv_state",
    -    }
    -    assert history["semantic_items"][0]["embedding"] == {"sqlite_blob_hex": "0001"}
    -
    -
    -def test_existing_proactive_quota_blocks_by_exact_bytes(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    content = b'{"version":1,"used":1,"window":"2026-07-12"}\n'
    -    (workspace / "proactive_quota.json").write_bytes(content)
    -
    -    inventory = inventory_workspace(workspace)
    -
    -    assert inventory.blocks[0].locator == "proactive:quota"
    -    assert inventory.blocks[0].reason == "proactive_quota_owner_unavailable"
    -    assert inventory.blocks[0].source_digest == hashlib.sha256(content).hexdigest()
    -
    -
    -@pytest.mark.parametrize(
    -    "table",
    -    [
    -        "reservoir_quarantine",
    -        "reservoir_tombstones",
    -        "hazard_state",
    -        "context_state",
    -        "context_reevaluate_state",
    -        "drift_state",
    -    ],
    -)
    -def test_real_wake_continuity_table_blocks_without_target_or_lineage(
    -    tmp_path: Path, table: str
    -) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    _populate_wake_continuity(workspace, table)
    -    before = _workspace_state(workspace)
    -
    -    planned = plan_cli(workspace)
    -    applied = apply_cli(workspace, (tmp_path / f"backup-{table}").resolve())
    -
    -    assert planned.status is HandoffStatus.BLOCK
    -    assert applied.status is HandoffStatus.BLOCK
    -    block = next(item for item in planned.items if item.locator == f"wake:{table}")
    -    assert block.reason == "wake_continuity_owner_unavailable"
    -    assert block.source_digest.startswith("rows=1;sha256=")
    -    assert _workspace_state(workspace) == before
    -    assert not (workspace / "runtime").exists()
    -    assert not (workspace / "plugin-data").exists()
    -    assert not (tmp_path / f"backup-{table}").exists()
    -
    -
    -def test_real_wake_schema_unknown_table_blocks(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    path = workspace / "wake_proactive.db"
    -    create_legacy_wake_database(path)
    -    connection = sqlite3.connect(path)
    -    connection.execute("CREATE TABLE future_wake_state(id TEXT, payload TEXT)")
    -    connection.execute("INSERT INTO future_wake_state VALUES('one', 'opaque')")
    -    connection.commit()
    -    connection.close()
    -    before = _workspace_state(workspace)
    -
    -    backup = (tmp_path / "unknown-table-backup").resolve()
    -    planned = plan_cli(workspace)
    -
    -    assert planned.status is HandoffStatus.BLOCK
    -    assert _workspace_state(workspace) == before
    -    assert not (workspace / "runtime").exists()
    -    assert not (
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    ).exists()
    -    assert not (workspace / "plugin-data").exists()
    -    assert not backup.exists()
    -    item = next(
    -        item for item in planned.items if item.locator == "wake:future_wake_state"
    -    )
    -    assert item.reason == "unknown_wake_table"
    -    assert len(item.source_digest) == 64
    -
    -    applied = apply_cli(workspace, backup)
    -
    -    assert applied.status is HandoffStatus.BLOCK
    -    assert any(item.reason == "unknown_wake_table" for item in applied.items)
    -    assert _workspace_state(workspace) == before
    -    assert not (workspace / "runtime").exists()
    -    assert not (
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    ).exists()
    -    assert not (workspace / "plugin-data").exists()
    -    assert not backup.exists()
    -
    -
    -def test_real_wake_history_tables_decode_without_blocking(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    path = workspace / "wake_proactive.db"
    -    now = datetime(2026, 8, 23, tzinfo=UTC)
    -    create_legacy_wake_database(path)
    -    connection = sqlite3.connect(path)
    -    connection.execute(
    -        "INSERT INTO wake_observations(wake_id, session_key, kind, now_utc, "
    -        "trigger_json, candidates_json, llm_input_json) VALUES(?,?,?,?,?,?,?)",
    -        (
    -            "wake:one",
    -            "wake:default",
    -            "fixture",
    -            now.isoformat(),
    -            json.dumps({"timer": "one"}),
    -            json.dumps([{"item_id": "one"}]),
    -            json.dumps([{"role": "user"}]),
    -        ),
    -    )
    -    connection.execute(
    -        "INSERT INTO hazard_monitor VALUES(?,?,?,?,?,?,?,?,?,?,?)",
    -        (
    -            "wake:default",
    -            0.1,
    -            0.2,
    -            0.1,
    -            0.5,
    -            0.3,
    -            0.2,
    -            "one",
    -            1,
    -            0,
    -            now.isoformat(),
    -        ),
    -    )
    -    connection.execute(
    -        "INSERT INTO wake_runs VALUES(?,?,?,?,?,?,?,?,?,?,?)",
    -        (
    -            "wake:one",
    -            "wake:default",
    -            now.isoformat(),
    -            "{}",
    -            "{}",
    -            "",
    -            "[]",
    -            "{}",
    -            "[]",
    -            0,
    -            "skip",
    -        ),
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -    inventory = inventory_workspace(workspace)
    -    history = LegacyProactiveHistory(workspace)
    -
    -    assert inventory.blocks == ()
    -    assert inventory.facts == ()
    -    assert history.wake_runs()[0]["wake_id"] == "wake:one"
    -    assert history.wake_observations()[0]["trigger"] == {"timer": "one"}
    -    assert history.wake_observations()[0]["candidates"] == [{"item_id": "one"}]
    -    assert history.wake_hazard_monitor()[0]["driver_item_id"] == "one"
    -
    -
    -def test_target_first_crash_replays_without_duplicate_content(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(1)])
    -    provider = tmp_path / "feed.sqlite3"
    -    _provider_db(provider, 2)
    -    content = EventMailStore(tmp_path / "content.sqlite3")
    -    content.initialize()
    -    adapter = _SourceAdapter(provider, content)
    -    inventory = inventory_workspace(workspace)
    -
    -    def crash(_fact: LegacyFact, _receipt: TargetReceipt) -> None:
    -        raise RuntimeError("crash after target")
    -
    -    with pytest.raises(RuntimeError, match="crash after target"):
    -        apply_handoff(workspace, inventory, (adapter,), after_target=crash)
    -    assert content.state_counts() == {"pending": 1}
    -    assert (
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    ).is_file()
    -    connection = sqlite3.connect(
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    )
    -    assert connection.execute("SELECT count(*) FROM lineage").fetchone()[0] == 0
    -    connection.close()
    -
    -    report = apply_handoff(workspace, inventory, (adapter,))
    -
    -    assert report.status is HandoffStatus.APPLIED
    -    assert adapter.apply_calls == 2
    -    assert content.state_counts() == {"pending": 1}
    -    connection = sqlite3.connect(
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    )
    -    assert connection.execute("SELECT count(*) FROM lineage").fetchone()[0] == 1
    -    assert connection.execute(
    -        "SELECT completed_at IS NOT NULL, count(*) FROM attempts "
    -        "GROUP BY completed_at IS NOT NULL ORDER BY completed_at IS NOT NULL"
    -    ).fetchall() == [(0, 1), (1, 1)]
    -    connection.close()
    -
    -    before_verify = _workspace_state(workspace)
    -    assert (
    -        preflight_handoff(workspace, inventory, (adapter,)).status
    -        is HandoffStatus.APPLIED
    -    )
    -    assert _workspace_state(workspace) == before_verify
    -
    -
    -def test_preflight_blocks_when_provider_replans_another_target(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(1)])
    -    provider = tmp_path / "feed.sqlite3"
    -    _provider_db(provider, 2)
    -    content = EventMailStore(tmp_path / "content.sqlite3")
    -    content.initialize()
    -    adapter = _SourceAdapter(provider, content)
    -    inventory = inventory_workspace(workspace)
    -    assert (
    -        apply_handoff(workspace, inventory, (adapter,)).status is HandoffStatus.APPLIED
    -    )
    -    connection = sqlite3.connect(provider)
    -    connection.execute(
    -        "UPDATE items SET content_hash='revision-changed' WHERE event_id='event-1'"
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -    report = preflight_handoff(workspace, inventory, (adapter,))
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert report.items[0].reason == "lineage_target_identity_drift"
    -
    -
    -def test_apply_keeps_the_preflight_target_when_provider_revision_drifts(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    _wake_db(workspace, [_wake_row(1)])
    -    provider = tmp_path / "feed.sqlite3"
    -    _provider_db(provider, 2)
    -    content = EventMailStore(tmp_path / "content.sqlite3")
    -    content.initialize()
    -    adapter = _SourceAdapter(provider, content)
    -    inventory = inventory_workspace(workspace)
    -    planned = preflight_handoff(workspace, inventory, (adapter,))
    -    connection = sqlite3.connect(provider)
    -    connection.execute(
    -        "UPDATE items SET content_hash='revision-changed' WHERE event_id='event-1'"
    -    )
    -    connection.commit()
    -    connection.close()
    -
    -    report = apply_handoff(
    -        workspace,
    -        inventory,
    -        (adapter,),
    -        planned=planned,
    -    )
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert report.items[0].reason == "target_plan_drift_before_apply"
    -    assert content.state_counts() == {}
    -    assert not (
    -        workspace / "runtime" / "proactive-island-handoff" / "lineage.sqlite3"
    -    ).exists()
    -
    -
    -def test_wake_rules_archive_keeps_exact_bytes_and_verified_lineage(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    content = "主动规则\n".encode()
    -    (workspace / "PROACTIVE_CONTEXT.md").write_bytes(content)
    -    inventory = inventory_workspace(workspace)
    -    adapter = WakeRulesArchiveAdapter(workspace)
    -
    -    assert (
    -        preflight_handoff(workspace, inventory, (adapter,)).status is HandoffStatus.PLAN
    -    )
    -    report = apply_handoff(workspace, inventory, (adapter,))
    -
    -    assert report.status is HandoffStatus.APPLIED
    -    archive = (
    -        workspace
    -        / "plugin-data"
    -        / "wake-builtin"
    -        / "legacy-rules"
    -        / "PROACTIVE_CONTEXT.md"
    -    )
    -    assert archive.read_bytes() == content
    -    assert plan_cli(workspace).status is HandoffStatus.APPLIED
    -
    -
    -def test_archived_rules_are_read_from_handoff_archive(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    (workspace / "PROACTIVE_CONTEXT.md").write_text(
    -        "\n# exact legacy rules\n\n", encoding="utf-8"
    -    )
    -    inventory = inventory_workspace(workspace)
    -    assert (
    -        apply_handoff(
    -            workspace, inventory, (WakeRulesArchiveAdapter(workspace),)
    -        ).status
    -        is HandoffStatus.APPLIED
    -    )
    -    assert read_archived_rules(
    -        workspace / "plugin-data" / "wake-builtin"
    -    ) == "# exact legacy rules"
    -
    -
    -@pytest.mark.parametrize(
    -    ("setup", "reason"),
    -    [
    -        ("drift", "proposal_payload_unrecoverable"),
    -        ("documents", "paired_target_handoff_unavailable"),
    -        ("pending", "pending_document_owner_unavailable"),
    -    ],
    -)
    -def test_unrecoverable_active_categories_block(
    -    tmp_path: Path, setup: str, reason: str
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    if setup == "drift":
    -        path = workspace / "drift" / "drift.db"
    -        path.parent.mkdir()
    -        connection = sqlite3.connect(path)
    -        connection.executescript(
    -            "CREATE TABLE skill_continuum(skill_name TEXT,last_status TEXT);"
    -            "CREATE TABLE runs(id INTEGER,event_id TEXT,message_result TEXT);"
    -            "INSERT INTO skill_continuum VALUES('paused-skill','paused');"
    -        )
    -        connection.commit()
    -        connection.close()
    -    elif setup == "documents":
    -        path = workspace / "runtime" / "proactive-documents" / "intents" / "one"
    -        path.mkdir(parents=True)
    -        (path / "intent.json").write_text("{}", encoding="utf-8")
    -    elif setup == "pending":
    -        (workspace / "proactive_pending.md").write_text("pending\n", encoding="utf-8")
    -    report = preflight_handoff(workspace, inventory_workspace(workspace), ())
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert any(item.reason == reason for item in report.items)
    -
    -
    -def test_historical_projection_never_creates_missing_legacy_state(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    before = tuple(workspace.rglob("*"))
    -
    -    snapshot = LegacyProactiveHistory(workspace).snapshot()
    -
    -    assert snapshot == {
    -        "proactive_tables": {},
    -        "wake_runs": (),
    -        "wake_observations": (),
    -        "wake_hazard_monitor": (),
    -        "drift_runs": (),
    -        "job_outcomes": (),
    -        "document_manifests": (),
    -    }
    -    assert tuple(workspace.rglob("*")) == before
    -
    -
    -def test_history_cli_projects_legacy_rows_without_writes(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    wake_path = _wake_db(workspace, [])
    -    connection = sqlite3.connect(wake_path)
    -    connection.execute(
    -        "INSERT INTO wake_runs VALUES(?,?,?,?,?,?,?,?,?,?,?)",
    -        (
    -            "wake:one",
    -            "wake:default",
    -            "2026-08-23T00:00:00+00:00",
    -            "{}",
    -            "[]",
    -            "hello",
    -            "[]",
    -            "{}",
    -            "[]",
    -            1,
    -            "sent",
    -        ),
    -    )
    -    connection.commit()
    -    connection.close()
    -    before = _workspace_state(workspace)
    -    monkeypatch.setattr(
    -        sys,
    -        "argv",
    -        ["proactive_island_handoff.py", "--workspace", str(workspace), "--history"],
    -    )
    -
    -    assert handoff_main() == 0
    -
    -    payload = json.loads(capsys.readouterr().out)
    -    assert payload["wake_runs"][0]["wake_id"] == "wake:one"
    -    assert _workspace_state(workspace) == before
    -
    -
    -def test_cli_apply_requires_backup_and_preserves_rules_source(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    workspace.mkdir()
    -    source = workspace / "PROACTIVE_CONTEXT.md"
    -    source.write_bytes(b"rules fixture\n")
    -    backup = tmp_path / "backup"
    -
    -    report = apply_cli(workspace, backup)
    -
    -    assert report.status is HandoffStatus.APPLIED
    -    assert source.read_bytes() == b"rules fixture\n"
    -    manifest = json.loads((backup / "manifest.json").read_text(encoding="utf-8"))
    -    assert (
    -        manifest["files"][0]["sha256"]
    -        == hashlib.sha256(source.read_bytes()).hexdigest()
    -    )
    -
    -
    -def test_backup_captures_proactive_database_and_quota(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    proactive = _proactive_db(workspace)
    -    quota = workspace / "proactive_quota.json"
    -    quota.write_text('{"version":1,"used":1}', encoding="utf-8")
    -    backup = tmp_path / "backup"
    -
    -    backup_sources(workspace, backup)
    -
    -    manifest = json.loads((backup / "manifest.json").read_text(encoding="utf-8"))
    -    assert any(entry["source"] == str(proactive) for entry in manifest["sqlite"])
    -    assert any(entry["source"] == str(quota) for entry in manifest["files"])
    -
    -
    -def test_retire_exact_approved_blocks_keeps_sources_and_requires_backup(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    _proactive_db(workspace)
    -    inventory = inventory_workspace(workspace)
    -    backup = (tmp_path / "backup").resolve()
    -
    -    report = retire_cli(workspace, backup, inventory_digest(inventory))
    -
    -    assert report.status is HandoffStatus.READY
    -    assert (workspace / "proactive.db").is_file()
    -    receipt = json.loads(
    -        (
    -            workspace / "runtime" / "proactive-island-handoff" / "retirement.json"
    -        ).read_text(encoding="utf-8")
    -    )
    -    assert receipt["decision"] == "operator_approved_pre_cutover_supersession"
    -    assert len(receipt["blocks"]) == 6
    -    assert plan_cli(workspace).status is HandoffStatus.READY
    -    assert (
    -        retire_cli(workspace, backup, inventory_digest(inventory)).status
    -        is HandoffStatus.READY
    -    )
    -
    -
    -def test_retirement_does_not_hide_changed_legacy_state(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    proactive = _proactive_db(workspace)
    -    inventory = inventory_workspace(workspace)
    -    _ = retire_cli(
    -        workspace,
    -        (tmp_path / "backup").resolve(),
    -        inventory_digest(inventory),
    -    )
    -    connection = sqlite3.connect(proactive)
    -    connection.execute("INSERT INTO seen_items VALUES('new-item', '2026-08-24')")
    -    connection.commit()
    -    connection.close()
    -
    -    report = plan_cli(workspace)
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert any(item.locator == "proactive:seen_items" for item in report.items)
    -
    -
    -def test_retire_inventory_digest_mismatch_writes_nothing(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    _proactive_db(workspace)
    -    before = _workspace_state(workspace)
    -    backup = (tmp_path / "backup").resolve()
    -
    -    report = retire_cli(workspace, backup, "0" * 64)
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert report.items[0].reason == "source_inventory_digest_mismatch"
    -    assert _workspace_state(workspace) == before
    -    assert not backup.exists()
    -
    -
    -def test_retire_rejects_unknown_block_before_backup(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    workspace.mkdir()
    -    connection = sqlite3.connect(workspace / "proactive.db")
    -    connection.execute("CREATE TABLE future_state(id TEXT)")
    -    connection.execute("INSERT INTO future_state VALUES('one')")
    -    connection.commit()
    -    connection.close()
    -    inventory = inventory_workspace(workspace)
    -    backup = (tmp_path / "backup").resolve()
    -
    -    with pytest.raises(RuntimeError, match="unknown_proactive_table"):
    -        retire_cli(workspace, backup, inventory_digest(inventory))
    -
    -    assert not backup.exists()
    -
    -
    -def test_retirement_fails_loud_when_recovery_artifact_changes(tmp_path: Path) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    _proactive_db(workspace)
    -    inventory = inventory_workspace(workspace)
    -    backup = (tmp_path / "backup").resolve()
    -    _ = retire_cli(workspace, backup, inventory_digest(inventory))
    -    (backup / "manifest.json").write_text("{}\n", encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="backup manifest changed"):
    -        plan_cli(workspace)
    -
    -
    -def test_apply_blocks_source_drift_after_backup_before_any_target_write(
    -    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
    -) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    workspace.mkdir()
    -    rules = workspace / "PROACTIVE_CONTEXT.md"
    -    rules.write_bytes(b"version one")
    -    original = handoff_cli.backup_sources
    -
    -    def backup_then_change(source: Path, target: Path) -> None:
    -        original(source, target)
    -        rules.write_bytes(b"version two")
    -
    -    monkeypatch.setattr(handoff_cli, "backup_sources", backup_then_change)
    -
    -    report = apply_cli(workspace, (tmp_path / "backup").resolve())
    -
    -    assert report.status is HandoffStatus.BLOCK
    -    assert report.items[0].reason == "source_inventory_drift_after_backup"
    -    assert not (workspace / "plugin-data").exists()
    -    assert not (workspace / "runtime").exists()
    -
    -
    -@pytest.mark.parametrize("relationship", ["relative", "equal", "child", "parent"])
    -def test_apply_requires_disjoint_absolute_backup_root(
    -    tmp_path: Path, relationship: str
    -) -> None:
    -    workspace = (tmp_path / "workspace").resolve()
    -    workspace.mkdir()
    -    (workspace / "PROACTIVE_CONTEXT.md").write_bytes(b"rules")
    -    backup = {
    -        "relative": Path("backup"),
    -        "equal": workspace,
    -        "child": workspace / "backup",
    -        "parent": tmp_path.resolve(),
    -    }[relationship]
    -
    -    with pytest.raises(ValueError):
    -        apply_cli(workspace, backup)
    -
    -
    -def _legacy_database_for_reader(workspace: Path, kind: str) -> Path:
    -    if kind == "proactive":
    -        return _proactive_db(workspace)
    -    if kind == "wake":
    -        return _wake_db(workspace, [])
    -    if kind == "drift":
    -        path = workspace / "drift" / "drift.db"
    -        path.parent.mkdir(parents=True)
    -        connection = sqlite3.connect(path)
    -        connection.executescript(
    -            "CREATE TABLE skill_continuum(skill_name TEXT,last_status TEXT);"
    -            "CREATE TABLE runs(id INTEGER,event_id TEXT,message_result TEXT);"
    -        )
    -        connection.commit()
    -        connection.close()
    -        return path
    -    path = workspace / "runtime" / "plugin-jobs" / "outcomes.sqlite"
    -    path.parent.mkdir(parents=True)
    -    connection = sqlite3.connect(path)
    -    connection.execute(
    -        "CREATE TABLE job_outcomes(invocation_id TEXT,created_at TEXT,"
    -        "event_payload_json TEXT)"
    -    )
    -    connection.commit()
    -    connection.close()
    -    return path
    -
    -
    -@pytest.mark.parametrize("kind", ["proactive", "wake", "drift", "jobs"])
    -def test_legacy_readers_reject_uncheckpointed_wal(tmp_path: Path, kind: str) -> None:
    -    workspace = tmp_path / "workspace"
    -    path = _legacy_database_for_reader(workspace, kind)
    -    path.with_name(path.name + "-wal").write_bytes(b"uncheckpointed frames")
    -
    -    with pytest.raises(RuntimeError, match="uncheckpointed WAL"):
    -        if kind == "jobs":
    -            LegacyProactiveHistory(workspace).job_outcomes()
    -        else:
    -            inventory_workspace(workspace)
    -
    -
    -@pytest.mark.parametrize("kind", ["proactive", "wake", "drift", "jobs"])
    -def test_legacy_readers_allow_empty_wal_and_existing_shm(
    -    tmp_path: Path, kind: str
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    path = _legacy_database_for_reader(workspace, kind)
    -    path.with_name(path.name + "-wal").write_bytes(b"")
    -    path.with_name(path.name + "-shm").write_bytes(b"retained shared memory")
    -
    -    if kind == "jobs":
    -        assert LegacyProactiveHistory(workspace).job_outcomes() == ()
    -    else:
    -        _ = inventory_workspace(workspace)
    diff --git a/tests/test_procedure_hint_semantic.py b/tests/test_procedure_hint_semantic.py
    deleted file mode 100644
    index 4355927e6..000000000
    --- a/tests/test_procedure_hint_semantic.py
    +++ /dev/null
    @@ -1,124 +0,0 @@
    -import asyncio
    -from pathlib import Path
    -from typing import Any, cast
    -from unittest.mock import MagicMock
    -
    -from agent.prompting import (
    -    PromptSectionRender,
    -    build_context_frame_content,
    -    build_context_frame_message,
    -    is_context_frame,
    -)
    -
    -from agent.looping.core import AgentLoop
    -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps, LLMConfig
    -from agent.context import ContextBuilder
    -from bus.queue import MessageBus
    -from agent.plugin_composition import LLMResponse, ToolCall
    -from agent.tools.base import Tool
    -from agent.tools.registry import ToolRegistry
    -from tests.memory_fakes import FakeMemoryEngine
    -from tests.provider_fakes import ProviderContextBudgetStub
    -from tests.compaction_fakes import run_test_agent_loop
    -
    -
    -class _DummyTool(Tool):
    -    def __init__(self, name: str = "shell") -> None:
    -        self._name = name
    -        self.calls: list[dict] = []
    -
    -    @property
    -    def name(self) -> str:
    -        return self._name
    -
    -    @property
    -    def description(self) -> str:
    -        return "dummy tool"
    -
    -    @property
    -    def parameters(self) -> dict:
    -        return {
    -            "type": "object",
    -            "properties": {"command": {"type": "string"}},
    -            "required": ["command"],
    -        }
    -
    -    async def execute(self, **kwargs) -> str:
    -        self.calls.append(kwargs)
    -        return "tool output"
    -
    -
    -class _FakeProvider(ProviderContextBudgetStub):
    -    def __init__(self, responses: list[LLMResponse]) -> None:
    -        self._responses = list(responses)
    -        self.calls: list[dict] = []
    -
    -    async def chat(self, **kwargs):
    -        self.calls.append(kwargs)
    -        return self._responses.pop(0)
    -
    -
    -def _make_loop(
    -    tmp_path: Path,
    -    provider: _FakeProvider,
    -    tool: Tool,
    -) -> AgentLoop:
    -    tools = ToolRegistry()
    -    tools.register(tool)
    -    loop = AgentLoop(
    -        AgentLoopDeps(
    -            bus=MessageBus(),
    -            tools=tools,
    -            session_manager=MagicMock(),
    -            workspace=tmp_path,
    -            context=ContextBuilder(tmp_path),
    -        ),
    -        AgentLoopConfig(llm=LLMConfig(max_iterations=5)),
    -    )
    -    return loop
    -
    -
    -def test_reflect_prompt_no_longer_contains_procedure_hint(tmp_path: Path):
    -    tool = _DummyTool()
    -    provider = _FakeProvider(
    -        [
    -            LLMResponse(
    -                content="",
    -                tool_calls=[ToolCall("c1", "shell", {"command": "pacman -S jq"})],
    -            ),
    -            LLMResponse(content="done", tool_calls=[]),
    -        ]
    -    )
    -    loop = _make_loop(tmp_path, provider, tool)
    -
    -    context_frame = build_context_frame_message(
    -        build_context_frame_content(
    -            [
    -                PromptSectionRender(
    -                    name="retrieved_memory",
    -                    content="已知上下文",
    -                    is_static=False,
    -                )
    -            ]
    -        )
    -    )
    -    asyncio.run(
    -        run_test_agent_loop(
    -            loop,
    -            provider,
    -            [context_frame, {"role": "user", "content": "test"}],
    -        )
    -    )
    -
    -    reflect_msgs = provider.calls[1]["messages"]
    -    all_content = " ".join(str(m.get("content", "")) for m in reflect_msgs)
    -    assert "【⚠️ 操作规范提醒 | 适用于本轮工具调用】" not in all_content
    -    # context frame 应作为 user 消息存在
    -    context_frame_msgs = [
    -        m
    -        for m in reflect_msgs
    -        if m.get("role") == "user" and is_context_frame(str(m.get("content", "")))
    -    ]
    -    assert (
    -        len(context_frame_msgs) > 0
    -    ), "expected at least one context frame user message"
    diff --git a/tests/test_production_sloc.py b/tests/test_production_sloc.py
    deleted file mode 100644
    index 06d7e5e8c..000000000
    --- a/tests/test_production_sloc.py
    +++ /dev/null
    @@ -1,148 +0,0 @@
    -from __future__ import annotations
    -
    -import importlib.util
    -from pathlib import Path
    -from types import ModuleType
    -from typing import Any
    -
    -ROOT = Path(__file__).resolve().parents[1]
    -
    -
    -def _sloc_module() -> ModuleType:
    -    path = ROOT / "scripts" / "measure_production_sloc.py"
    -    spec = importlib.util.spec_from_file_location("production_sloc", path)
    -    if spec is None or spec.loader is None:
    -        raise RuntimeError(f"无法加载 {path}")
    -    module = importlib.util.module_from_spec(spec)
    -    spec.loader.exec_module(module)
    -    return module
    -
    -
    -def test_python_sloc_excludes_docstrings_and_comments_but_counts_real_strings() -> None:
    -    sloc = _sloc_module()
    -    source = '''
    -"""模块说明\n第二行说明\n"""
    -# 独立注释
    -def render():
    -    """函数说明\n    函数说明第二行\n    """
    -    value = """真实字符串 # 不是注释\n    真实字符串第二行\n    """
    -    return value  # 行内注释不抹掉代码
    -'''
    -
    -    assert sloc.count_python_sloc(source) == 5
    -
    -
    -def test_python_standalone_string_in_non_docstring_block_is_counted() -> None:
    -    sloc = _sloc_module()
    -    source = "if enabled:\n    \"runtime marker\"\n"
    -
    -    assert sloc.count_python_sloc(source) == 2
    -
    -
    -def test_python_code_after_single_line_docstring_is_not_removed() -> None:
    -    sloc = _sloc_module()
    -
    -    assert sloc.count_python_sloc('"""模块说明"""; VALUE = 1\n') == 1
    -
    -
    -def test_python_parenthesised_concatenated_docstring_is_excluded() -> None:
    -    sloc = _sloc_module()
    -    source = '''
    -def render():
    -    (
    -        "第一段说明"
    -        "第二段说明"
    -    )
    -    return "value"
    -'''
    -
    -    assert sloc.count_python_sloc(source) == 2
    -
    -
    -def test_typescript_lexer_keeps_comment_markers_inside_strings() -> None:
    -    sloc = _sloc_module()
    -    source = '''
    -const text = `第一行 // 仍是字符串
    -第二行 /* 仍是字符串 */
    -`;
    -/* 独立块注释
    -   第二行注释 */
    -const marker = "/* // 都是字符串";
    -// 独立行注释
    -return marker;
    -'''
    -
    -    assert sloc.count_typescript_sloc(source) == 5
    -
    -
    -def test_typescript_template_interpolation_excludes_comment_only_lines() -> None:
    -    sloc = _sloc_module()
    -    source = """const text = `${
    -// 插值注释
    -value
    -}`;
    -"""
    -
    -    assert sloc.count_typescript_sloc(source) == 3
    -
    -
    -def test_tracked_files_ignore_unstaged_deletions(
    -    tmp_path: Path, monkeypatch: Any
    -) -> None:
    -    sloc = _sloc_module()
    -    kept = tmp_path / "agent" / "kept.py"
    -    kept.parent.mkdir()
    -    kept.write_text("VALUE = 1\n", encoding="utf-8")
    -    git_output = b"agent/kept.py\0agent/deleted.py\0"
    -    monkeypatch.setattr(sloc, "ROOT", tmp_path)
    -    monkeypatch.setattr(
    -        sloc.subprocess,
    -        "run",
    -        lambda *_args, **_kwargs: sloc.subprocess.CompletedProcess(
    -            ["git", "ls-files"], 0, git_output, b""
    -        ),
    -    )
    -
    -    assert sloc._tracked_files() == ["agent/kept.py"]
    -
    -
    -def test_source_set_includes_only_approved_production_extensions_and_roots() -> None:
    -    sloc = _sloc_module()
    -    included = (
    -        "main.py",
    -        "agent/core/runtime.py",
    -        "sdk/python/src/sdk.py",
    -        "frontend/chat/src/main.tsx",
    -    )
    -    excluded = (
    -        "tests/test_runtime.py",
    -        "eval/runner.py",
    -        "docker/debug/gate.py",
    -        "scripts/measure_production_sloc.py",
    -        "frontend/chat/src/styles.css",
    -        "frontend/chat/src/types.d.ts",
    -        "frontend/chat/dist/bundle.js",
    -    )
    -
    -    assert all(sloc.is_production_source_path(path) for path in included)
    -    assert all(not sloc.is_production_source_path(path) for path in excluded)
    -    assert sloc.production_source_root("frontend/chat/src/main.tsx") == (
    -        "frontend/chat/src"
    -    )
    -    assert sloc.production_source_root("migrations/20260722_example/migration.py") == (
    -        "migrations"
    -    )
    -
    -
    -def test_measurement_report_has_stable_language_root_and_total_fields() -> None:
    -    sloc = _sloc_module()
    -    report = sloc.measure()
    -    by_language = report["sloc"]["byLanguage"]
    -    by_root = report["sloc"]["byRoot"]
    -
    -    assert report["version"] == 1
    -    assert report["fileCount"] > 0
    -    assert len(report["sourceSetDigest"]) == 64
    -    assert set(by_language) == {"python", "typescript"}
    -    assert report["total"] == sum(by_language.values())
    -    assert report["total"] == sum(by_root.values())
    diff --git a/tests/test_recall_memory_tool.py b/tests/test_recall_memory_tool.py
    deleted file mode 100644
    index 16ddfcc30..000000000
    --- a/tests/test_recall_memory_tool.py
    +++ /dev/null
    @@ -1,98 +0,0 @@
    -import json
    -from datetime import datetime
    -from typing import Any, cast
    -from zoneinfo import ZoneInfo
    -
    -import pytest
    -
    -import agent.tools.recall_memory as recall_memory_module
    -from agent.tools.recall_memory import RecallMemoryTool
    -from core.memory.engine import (
    -    EvidenceRef,
    -    MemoryQueryResult,
    -    MemoryRecord,
    -    MemoryToolSpec,
    -)
    -
    -
    -class _CaptureMemory:
    -    request = None
    -
    -    async def query(self, request):
    -        self.request = request
    -        return MemoryQueryResult()
    -
    -
    -@pytest.mark.asyncio
    -async def test_recall_memory_passes_current_timestamp_to_engine() -> None:
    -    memory = _CaptureMemory()
    -    tool = RecallMemoryTool(
    -        cast(Any, memory),
    -        MemoryToolSpec(description="", parameters={"type": "object", "properties": {}}),
    -    )
    -    timestamp = datetime(2026, 4, 4, 22, 0, 0)
    -
    -    _ = await tool.execute(query="Akasha", current_timestamp=timestamp.isoformat())
    -
    -    assert memory.request.timestamp == timestamp
    -
    -
    -def test_parse_time_filter_supports_presets_and_ranges(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    timezone = ZoneInfo("Asia/Shanghai")
    -    monkeypatch.setattr(
    -        recall_memory_module,
    -        "_now_local",
    -        lambda: datetime(2026, 4, 25, 15, 30, tzinfo=timezone),
    -    )
    -
    -    assert recall_memory_module._parse_time_filter("today") == (
    -        datetime(2026, 4, 25, 0, 0, tzinfo=timezone),
    -        datetime(2026, 4, 26, 0, 0, tzinfo=timezone),
    -    )
    -    assert recall_memory_module._parse_time_filter("recent_3d") == (
    -        datetime(2026, 4, 22, 15, 30, tzinfo=timezone),
    -        datetime(2026, 4, 25, 15, 30, tzinfo=timezone),
    -    )
    -    assert recall_memory_module._parse_time_filter("2026-04-20") == (
    -        datetime(2026, 4, 20, 0, 0, tzinfo=timezone),
    -        datetime(2026, 4, 21, 0, 0, tzinfo=timezone),
    -    )
    -    assert recall_memory_module._parse_time_filter("2026-04-20~2026-04-25") == (
    -        datetime(2026, 4, 20, 0, 0, tzinfo=timezone),
    -        datetime(2026, 4, 26, 0, 0, tzinfo=timezone),
    -    )
    -
    -
    -def test_recall_memory_response_preserves_activation_metadata() -> None:
    -    payload = json.loads(
    -        recall_memory_module._render_records(
    -            [
    -                MemoryRecord(
    -                    id="mem:1",
    -                    kind="event",
    -                    summary="用户提到 Falcons 比赛",
    -                    score=0.704,
    -                    engine_kind="akasha",
    -                    evidence=[EvidenceRef(refs=["msg:1"], source_ref="msg:1")],
    -                    signals={
    -                        "cosine": 0.81,
    -                        "lambda_before": 0.2,
    -                        "lambda_after": 0.9,
    -                        "activation": 0.9,
    -                        "activated": True,
    -                    },
    -                )
    -            ],
    -            trace={},
    -        )
    -    )
    -
    -    assert payload["items"][0]["signals"] == {
    -        "cosine": 0.81,
    -        "lambda_before": 0.2,
    -        "lambda_after": 0.9,
    -        "activation": 0.9,
    -        "activated": True,
    -    }
    diff --git a/tests/test_release_source_identity.py b/tests/test_release_source_identity.py
    deleted file mode 100644
    index 8a10ed5a0..000000000
    --- a/tests/test_release_source_identity.py
    +++ /dev/null
    @@ -1,91 +0,0 @@
    -from __future__ import annotations
    -
    -import importlib.util
    -import json
    -from pathlib import Path
    -from types import ModuleType
    -
    -import pytest
    -
    -
    -def _module() -> ModuleType:
    -    path = (
    -        Path(__file__).parents[1]
    -        / "docker"
    -        / "host-runtime"
    -        / "verify_release_source.py"
    -    )
    -    spec = importlib.util.spec_from_file_location("verify_release_source", path)
    -    assert spec is not None and spec.loader is not None
    -    module = importlib.util.module_from_spec(spec)
    -    spec.loader.exec_module(module)
    -    return module
    -
    -
    -def _manifest(module: ModuleType, root: Path) -> tuple[Path, dict[str, object]]:
    -    document: dict[str, object] = {
    -        "schemaVersion": 1,
    -        "sourceCommit": "a" * 40,
    -        "sourceTree": "b" * 40,
    -        "sourceArchiveSha256": "c" * 64,
    -        "files": module.source_entries(root),
    -    }
    -    path = root / ".akashic-source-manifest.json"
    -    path.write_text(json.dumps(document), encoding="utf-8")
    -    return path, document
    -
    -
    -def test_release_source_verifies_exact_archive_tree(tmp_path: Path) -> None:
    -    module = _module()
    -    (tmp_path / "agent.py").write_text("stable\n", encoding="utf-8")
    -    manifest, document = _manifest(module, tmp_path)
    -
    -    assert (
    -        module.verify_release_source(
    -            tmp_path,
    -            manifest,
    -            expected_commit="a" * 40,
    -            expected_tree="b" * 40,
    -            expected_archive_sha256="c" * 64,
    -        )
    -        == document
    -    )
    -
    -
    -@pytest.mark.parametrize("mutation", ["changed", "extra"])
    -def test_release_source_rejects_dirty_or_untracked_context(
    -    tmp_path: Path, mutation: str
    -) -> None:
    -    module = _module()
    -    source = tmp_path / "agent.py"
    -    source.write_text("stable\n", encoding="utf-8")
    -    manifest, _ = _manifest(module, tmp_path)
    -    if mutation == "changed":
    -        source.write_text("dirty\n", encoding="utf-8")
    -    else:
    -        (tmp_path / "config.toml").write_text("secret=true\n", encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="Docker build context"):
    -        module.verify_release_source(
    -            tmp_path,
    -            manifest,
    -            expected_commit="a" * 40,
    -            expected_tree="b" * 40,
    -            expected_archive_sha256="c" * 64,
    -        )
    -
    -
    -def test_runtime_image_prefers_domestic_package_cache_with_archive_fallback() -> None:
    -    dockerfile = (
    -        Path(__file__).parents[1] / "docker" / "host-runtime" / "Dockerfile"
    -    ).read_text(encoding="utf-8")
    -
    -    tuna = dockerfile.index("CacheServer = https://mirrors.tuna.tsinghua.edu.cn")
    -    ustc = dockerfile.index("CacheServer = https://mirrors.ustc.edu.cn")
    -    archive = dockerfile.index("Server = https://archive.archlinux.org/repos/")
    -    assert tuna < ustc < archive
    -    assert dockerfile.count("pacman --disable-download-timeout") == 2
    -    assert "https://mirrors.aliyun.com/pypi/simple" in dockerfile
    -    assert '--index-url "${AKASHIC_PYPI_INDEX_URL}"' in dockerfile
    -    assert "https://registry.npmmirror.com" in dockerfile
    -    assert '--registry "${AKASHIC_NPM_REGISTRY}"' in dockerfile
    diff --git a/tests/test_replay_debug_manifest.py b/tests/test_replay_debug_manifest.py
    deleted file mode 100644
    index 47654de16..000000000
    --- a/tests/test_replay_debug_manifest.py
    +++ /dev/null
    @@ -1,56 +0,0 @@
    -import tomllib
    -from pathlib import Path
    -
    -from agent.plugins.static_manifest import (
    -    load_static_plugin_manifest,
    -    materialize_static_command,
    -)
    -
    -
    -def test_replay_debug_manifest_binds_artifact_python_runtime() -> None:
    -    manifest_path = (
    -        Path(__file__).parents[1]
    -        / "docker"
    -        / "debug"
    -        / "plugins"
    -        / "replay_debug"
    -        / "akashic.plugin.toml"
    -    )
    -    manifest = tomllib.loads(manifest_path.read_text(encoding="utf-8"))
    -
    -    command = manifest["mcp"][0]["command"]
    -
    -    assert command == ["python", "replay_mcp.py"]
    -    assert manifest["python"] == [{"requirements": "requirements.txt"}]
    -
    -
    -def test_replay_debug_materializes_its_staged_interpreter(tmp_path: Path) -> None:
    -    source = (
    -        Path(__file__).parents[1]
    -        / "docker"
    -        / "debug"
    -        / "plugins"
    -        / "replay_debug"
    -    )
    -    artifact = tmp_path / "replay_debug"
    -    artifact.mkdir()
    -    for name in (
    -        "akashic.plugin.toml",
    -        "plugin.py",
    -        "replay_mcp.py",
    -        "requirements.txt",
    -    ):
    -        (artifact / name).write_bytes((source / name).read_bytes())
    -    interpreter = artifact / ".venv" / "bin" / "python"
    -    interpreter.parent.mkdir(parents=True)
    -    interpreter.write_text("#!/bin/sh\n", encoding="utf-8")
    -    interpreter.chmod(0o755)
    -
    -    manifest = load_static_plugin_manifest(artifact)
    -    command = materialize_static_command(
    -        artifact,
    -        manifest,
    -        manifest.mcp_servers[0],
    -    )
    -
    -    assert command == (str(interpreter), "replay_mcp.py")
    diff --git a/tests/test_runtime_identity.py b/tests/test_runtime_identity.py
    deleted file mode 100644
    index 6782c7c4e..000000000
    --- a/tests/test_runtime_identity.py
    +++ /dev/null
    @@ -1,124 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import subprocess
    -from pathlib import Path
    -
    -import pytest
    -
    -from agent.runtime_identity import RuntimeIdentity
    -
    -
    -def _runtime_info(commit: str, tree: str) -> dict[str, object]:
    -    return {
    -        "schemaVersion": 2,
    -        "sourceCommit": commit,
    -        "sourceTree": tree,
    -        "sourceArchiveSha256": "a" * 64,
    -        "sourceManifestSha256": "b" * 64,
    -        "baseImage": "archlinux@sha256:" + "c" * 64,
    -        "archSnapshot": "2026/08/10",
    -        "pacmanDigest": "d" * 64,
    -        "requirementsLockSha256": "e" * 64,
    -        "packageLockSha256": "f" * 64,
    -        "pythonVersion": "3.14.0",
    -        "nodeVersion": "v22.23.1",
    -        "npmVersion": "11.0.0",
    -    }
    -
    -
    -def _release_manifest(tmp_path: Path, runtime_info: dict[str, object]) -> Path:
    -    path = tmp_path / "release.json"
    -    path.write_text(
    -        json.dumps(
    -            {
    -                "schemaVersion": 1,
    -                "imageId": "sha256:" + "1" * 64,
    -                "runtimeInfo": runtime_info,
    -            }
    -        ),
    -        encoding="utf-8",
    -    )
    -    return path
    -
    -
    -def _checkout(tmp_path: Path) -> tuple[Path, str, str]:
    -    checkout = tmp_path / "checkout"
    -    checkout.mkdir()
    -    subprocess.run(["git", "init", "-q", str(checkout)], check=True)
    -    subprocess.run(
    -        ["git", "-C", str(checkout), "config", "user.email", "test@example.com"],
    -        check=True,
    -    )
    -    subprocess.run(
    -        ["git", "-C", str(checkout), "config", "user.name", "Test"],
    -        check=True,
    -    )
    -    (checkout / "main.py").write_text("print('ok')\n", encoding="utf-8")
    -    subprocess.run(["git", "-C", str(checkout), "add", "main.py"], check=True)
    -    subprocess.run(["git", "-C", str(checkout), "commit", "-qm", "fixture"], check=True)
    -    commit = subprocess.check_output(
    -        ["git", "-C", str(checkout), "rev-parse", "HEAD"], text=True
    -    ).strip()
    -    tree = subprocess.check_output(
    -        ["git", "-C", str(checkout), "rev-parse", "HEAD^{tree}"], text=True
    -    ).strip()
    -    return checkout, commit, tree
    -
    -
    -def test_runtime_identity_requires_image_and_deployment_commit_match(
    -    tmp_path: Path,
    -) -> None:
    -    checkout, commit, tree = _checkout(tmp_path)
    -    info = tmp_path / "runtime-info.json"
    -    runtime_document = _runtime_info(commit, tree)
    -    info.write_text(
    -        json.dumps(runtime_document),
    -        encoding="utf-8",
    -    )
    -
    -    identity = RuntimeIdentity.load(
    -        info,
    -        _release_manifest(tmp_path, runtime_document),
    -        expected_commit=commit,
    -        host_checkout=checkout,
    -    )
    -
    -    assert identity.source_commit == commit
    -    assert identity.source_tree == tree
    -    assert identity.source_archive_sha256 == "a" * 64
    -    assert len(identity.environment_digest) == 64
    -
    -
    -def test_runtime_identity_rejects_mismatched_commit(tmp_path: Path) -> None:
    -    checkout, commit, tree = _checkout(tmp_path)
    -    info = tmp_path / "runtime-info.json"
    -    runtime_document = _runtime_info(commit, tree)
    -    info.write_text(
    -        json.dumps(runtime_document),
    -        encoding="utf-8",
    -    )
    -
    -    with pytest.raises(RuntimeError, match="runtime commit 不一致"):
    -        RuntimeIdentity.load(
    -            info,
    -            _release_manifest(tmp_path, runtime_document),
    -            expected_commit="c" * 40,
    -            host_checkout=checkout,
    -        )
    -
    -
    -def test_runtime_identity_rejects_unpinned_environment(tmp_path: Path) -> None:
    -    checkout, commit, tree = _checkout(tmp_path)
    -    document = _runtime_info(commit, tree)
    -    document["baseImage"] = "archlinux:latest"
    -    info = tmp_path / "runtime-info.json"
    -    info.write_text(json.dumps(document), encoding="utf-8")
    -
    -    with pytest.raises(RuntimeError, match="baseImage 必须固定 digest"):
    -        RuntimeIdentity.load(
    -            info,
    -            _release_manifest(tmp_path, document),
    -            expected_commit=commit,
    -            host_checkout=checkout,
    -        )
    diff --git a/tests/test_runtime_inspection.py b/tests/test_runtime_inspection.py
    deleted file mode 100644
    index 901a71679..000000000
    --- a/tests/test_runtime_inspection.py
    +++ /dev/null
    @@ -1,227 +0,0 @@
    -from __future__ import annotations
    -
    -from datetime import datetime, timezone
    -from pathlib import Path
    -from types import SimpleNamespace
    -from typing import cast
    -
    -import pytest
    -
    -from infra.mobile_realtime.runtime_inspection import (
    -    RuntimeInspectionError,
    -    RuntimeInspectionService,
    -    _mcp_items,
    -)
    -from agent.plugins.snapshot import RuntimeSnapshot
    -from agent.plugins.manager import PluginManager
    -from agent.scheduler import JobStore, ScheduledJob
    -from agent.tools.base import Tool
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -
    -
    -def _service(tmp_path: Path) -> tuple[RuntimeInspectionService, JobStore]:
    -    store = JobStore(tmp_path / "schedules.json")
    -    service = RuntimeInspectionService(
    -        workspace=tmp_path,
    -        snapshot_store=None,
    -    )
    -    return service, store
    -
    -
    -def _write_plugin(root: Path, name: str, source: str) -> None:
    -    plugin_dir = root / name
    -    plugin_dir.mkdir(parents=True)
    -    (plugin_dir / "plugin.py").write_text(source, encoding="utf-8")
    -
    -
    -class _InspectionMcpTool(Tool):
    -    @property
    -    def name(self) -> str:
    -        return "mcp_calendar__list_events"
    -
    -    @property
    -    def description(self) -> str:
    -        return "[MCP:calendar] List calendar events"
    -
    -    @property
    -    def parameters(self) -> dict[str, object]:
    -        return {
    -            "type": "object",
    -            "properties": {"date": {"type": "string"}},
    -        }
    -
    -    async def execute(self, **kwargs: object) -> str:
    -        return "unused"
    -
    -
    -def test_documents_use_fixed_allowlist_and_return_markdown(tmp_path: Path) -> None:
    -    service, _ = _service(tmp_path)
    -    path = tmp_path / "memory/MEMORY.md"
    -    path.parent.mkdir(parents=True)
    -    path.write_text("# Memory\n\n真实内容", encoding="utf-8")
    -
    -    listed = service.list_documents()
    -    document = service.get_document("memory")
    -
    -    assert len(cast(list[object], listed["items"])) == 3
    -    assert document["relative_path"] == "memory/MEMORY.md"
    -    assert document["markdown"] == "# Memory\n\n真实内容"
    -    with pytest.raises(RuntimeInspectionError, match="未知运行时文档"):
    -        service.get_document("../../config.toml")
    -    with pytest.raises(RuntimeInspectionError, match="未知运行时文档"):
    -        service.get_document("proactive-context")
    -
    -
    -def test_scheduler_projection_reads_live_service_state(tmp_path: Path) -> None:
    -    service, store = _service(tmp_path)
    -    job = ScheduledJob(
    -        id="morning",
    -        name="晨间提醒",
    -        trigger="every",
    -        tier="instant",
    -        fire_at=datetime(2026, 7, 29, tzinfo=timezone.utc),
    -        channel="mobile",
    -        chat_id="mobile:test",
    -        interval_seconds=3600,
    -        message="起来走一走",
    -    )
    -    store.save({job.id: job})
    -
    -    listed = service.list_jobs()
    -    detail = service.get_job("morning")
    -
    -    assert cast(list[dict[str, object]], listed["items"])[0]["id"] == "morning"
    -    assert "起来走一走" in cast(str, detail["markdown"])
    -    with pytest.raises(RuntimeInspectionError, match="定时任务不存在"):
    -        service.get_job("missing")
    -
    -
    -@pytest.mark.asyncio
    -async def test_capabilities_fail_loud_without_runtime_snapshot(tmp_path: Path) -> None:
    -    service, _ = _service(tmp_path)
    -
    -    with pytest.raises(RuntimeInspectionError, match="快照尚未就绪"):
    -        await service.list_capabilities()
    -
    -
    -def test_mcp_projection_uses_exact_v3_registry_and_live_tool_view() -> None:
    -    registry = ToolRegistry()
    -    registry.register(
    -        _InspectionMcpTool(),
    -        source_type="mcp",
    -        source_name="calendar",
    -    )
    -    snapshot = cast(
    -        RuntimeSnapshot,
    -        SimpleNamespace(
    -            mcp_server_registry=SimpleNamespace(
    -                descriptors=(
    -                    SimpleNamespace(owner="calendar-plugin", name="calendar"),
    -                )
    -            ),
    -            tool_registry=registry,
    -        ),
    -    )
    -
    -    assert _mcp_items(snapshot) == [
    -        {
    -            "owner_id": "calendar-plugin",
    -            "name": "calendar",
    -            "tool_count": 1,
    -            "tools": [
    -                {
    -                    "name": "list_events",
    -                    "description": "List calendar events",
    -                    "input_schema": {
    -                        "type": "object",
    -                        "properties": {"date": {"type": "string"}},
    -                    },
    -                }
    -            ],
    -        }
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -async def test_capabilities_project_bounded_v3_composition_facts(
    -    tmp_path: Path,
    -) -> None:
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "inspected_v3",
    -        "api_version = 3\n"
    -        "name = 'inspected_v3'\n"
    -        "version = '1.0.0'\n"
    -        "async def worker(ctx):\n"
    -        "    health = await ctx.health('poller', required=False)\n"
    -        "    health.degrade('paused')\n"
    -        "    for index in range(140):\n"
    -        "        ctx.report_incident('poll', f'failure {index}')\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.mount(worker, name='worker', required_for_readiness=False)\n",
    -    )
    -    _write_plugin(
    -        tmp_path / "plugins",
    -        "inactive_v3",
    -        "api_version = 3\n"
    -        "name = 'inactive_v3'\n"
    -        "version = '1.0.0'\n"
    -        "def is_active(services): return False\n"
    -        "async def worker(ctx): pass\n"
    -        "async def apply(ctx, config):\n"
    -        "    await ctx.mount(worker, name='inactive_worker')\n",
    -    )
    -    workspace = tmp_path / "workspace"
    -    manager = PluginManager(
    -        plugin_dirs=[tmp_path / "plugins"],
    -        event_bus=EventBus(),
    -        tool_registry=None,
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "home" / "cache",
    -    )
    -    service = RuntimeInspectionService(
    -        workspace=workspace,
    -        snapshot_store=manager.snapshot_store,
    -    )
    -    await manager.load_all()
    -
    -    payload = await service.list_capabilities()
    -
    -    plugins = {
    -        cast(str, item["id"]): item
    -        for item in cast(list[dict[str, object]], payload["plugins"])
    -    }
    -    assert "inactive_v3" not in plugins
    -    v3 = plugins["inspected_v3"]
    -    assert v3["api_version"] == 3
    -    composition = cast(dict[str, object], v3["composition"])
    -    assert composition["ready"] is True
    -    fibers = cast(list[dict[str, object]], composition["fibers"])
    -    assert [(item["name"], item["parent"]) for item in fibers] == [
    -        ("inspected_v3", None),
    -        ("worker", "inspected_v3"),
    -    ]
    -    health = cast(list[dict[str, object]], composition["health"])
    -    assert health == [
    -        {
    -            "owner": "worker",
    -            "name": "poller",
    -            "required": False,
    -            "healthy": False,
    -            "reason": "paused",
    -        }
    -    ]
    -    assert composition["incident_count"] == 140
    -    incidents = cast(
    -        list[dict[str, object]],
    -        composition["recent_incidents"],
    -    )
    -    assert len(incidents) == 20
    -    assert incidents[0]["message"] == "failure 120"
    -    assert incidents[-1]["message"] == "failure 139"
    -
    -    await manager.terminate_all()
    -
    -    with pytest.raises(RuntimeInspectionError, match="快照尚未就绪"):
    -        await service.list_capabilities()
    diff --git a/tests/test_safety_retry_service.py b/tests/test_safety_retry_service.py
    deleted file mode 100644
    index 404ca786b..000000000
    --- a/tests/test_safety_retry_service.py
    +++ /dev/null
    @@ -1,250 +0,0 @@
    -import asyncio
    -import json
    -from datetime import datetime, timezone
    -from types import SimpleNamespace
    -from typing import Any, cast
    -from unittest.mock import AsyncMock
    -
    -from agent.core.passive_turn import DefaultReasoner
    -from agent.core.runtime_support import ToolDiscoveryState
    -from agent.core.types import ContextRequest, ReasonerResult
    -from agent.looping.ports import LLMConfig
    -from plugins.compaction.engine import (
    -    CommittedContextUnit,
    -    ContextPayloadSegments,
    -)
    -from agent.plugin_composition import ContentSafetyError, ContextLengthError, ModelRole
    -from agent.prompting import AssembledTurnInput
    -from plugins.compaction.runtime import CompactionProjection
    -from session.store import CompactionHead
    -from tests.model_plugin_fakes import BoundChatModelFake
    -
    -
    -class _ProviderContextBudget:
    -    """Expose the provider budget contract required by the compaction gate."""
    -
    -    context_window = 100_000
    -    runtime_id = "safety-retry-test"
    -
    -    def estimate_context_tokens(
    -        self,
    -        messages: list[dict],
    -        tools: list[dict],
    -    ) -> int:
    -        return max(1, len(json.dumps([messages, tools], ensure_ascii=False)) // 3)
    -
    -    def estimate_appended_message_tokens(self, messages: list[dict]) -> int:
    -        if not messages:
    -            return 0
    -        return max(1, len(json.dumps(messages, ensure_ascii=False)) // 3)
    -
    -    async def chat(self, **_: object) -> None:
    -        raise AssertionError("run_turn test must not bypass the mocked reasoner.run")
    -
    -
    -def _stub_turn_injection_context(
    -    *, turn_injection_prompt: str | None = None
    -) -> dict[str, str]:
    -    if not turn_injection_prompt:
    -        return {}
    -    return {"turn_injection": turn_injection_prompt}
    -
    -
    -def _msg():
    -    return SimpleNamespace(
    -        content="hello",
    -        media=[],
    -        channel="cli",
    -        chat_id="1",
    -        timestamp=datetime.now(timezone.utc),
    -    )
    -
    -
    -def _session():
    -    history = [{"role": "user", "content": str(i)} for i in range(6)]
    -    return SimpleNamespace(
    -        key="s:1",
    -        created_at=datetime(2026, 8, 8, tzinfo=timezone.utc),
    -        messages=history,
    -        get_history=lambda max_messages=500: [
    -            dict(message) for message in history
    -        ],
    -        last_consolidated=3,
    -    )
    -
    -
    -def _make_reasoner(
    -    *,
    -    discovery: ToolDiscoveryState,
    -    tool_search_enabled: bool,
    -    render: object | None = None,
    -):
    -    def _render(request: ContextRequest, **kwargs: object) -> AssembledTurnInput:
    -        return AssembledTurnInput(
    -            system_prompt="test context",
    -            turn_injection_context=_stub_turn_injection_context(
    -                turn_injection_prompt=request.turn_injection_prompt
    -            ),
    -            messages=[
    -                {"role": "system", "content": "test context"},
    -                *list(request.history),
    -                {"role": "user", "content": request.current_message},
    -            ],
    -            debug_breakdown=[],
    -        )
    -
    -    provider = _ProviderContextBudget()
    -    reasoner = DefaultReasoner(
    -        llm_config=LLMConfig(max_iterations=4, max_tokens=256),
    -        tools=cast(
    -            Any,
    -            SimpleNamespace(
    -                get_always_on_names=lambda: {"always"},
    -                get_registered_order=lambda names=None: sorted(names or ()),
    -                get_deferred_names=lambda visible=None: {
    -                    "builtin": [],
    -                    "mcp": {},
    -                },
    -                get_schemas=lambda names=None: [],
    -                get_tool=lambda name: None,
    -            ),
    -        ),
    -        discovery=discovery,
    -        tool_search_enabled=tool_search_enabled,
    -        context=cast(Any, SimpleNamespace(render=render or _render)),
    -    )
    -    reasoner._test_agent_model = BoundChatModelFake(provider, model="m")
    -    reasoner._test_fallback_model = BoundChatModelFake(
    -        provider,
    -        model="m",
    -        role=ModelRole.DEFAULT,
    -    )
    -    return reasoner
    -
    -
    -def _run_turn(reasoner: DefaultReasoner, session: object):
    -    return reasoner.run_turn(
    -        msg=_msg(),
    -        session=cast(Any, session),
    -        agent_model=reasoner._test_agent_model,
    -        fallback_model=reasoner._test_fallback_model,
    -    )
    -
    -
    -def test_reasoner_run_turn_content_safety_returns_user_error_without_retry():
    -    discovery = ToolDiscoveryState()
    -    discovery.update("s:1", ["old"], set())
    -    reasoner = _make_reasoner(discovery=discovery, tool_search_enabled=True)
    -    reasoner.run = AsyncMock(side_effect=ContentSafetyError("blocked"))
    -
    -    session = _session()
    -    original_messages = list(session.messages)
    -    result = asyncio.run(_run_turn(reasoner, session))
    -
    -    assert result.reply == "你的消息触发了安全审查,无法处理。"
    -    assert reasoner.run.await_count == 1
    -    assert result.context_retry["selected_plan"] is None
    -    assert result.context_retry["attempts"] == [
    -        {
    -            "name": "full_context",
    -            "history_window": 6,
    -            "disabled_sections": [],
    -        }
    -    ]
    -    assert "x" not in discovery._unlocked["s:1"]
    -    assert session.messages == original_messages
    -    assert session.last_consolidated == 3
    -
    -
    -def test_reasoner_run_turn_success_updates_discovery_with_full_context_plan():
    -    discovery = ToolDiscoveryState()
    -    discovery.update("s:1", ["old"], set())
    -    reasoner = _make_reasoner(discovery=discovery, tool_search_enabled=True)
    -    reasoner.run = AsyncMock(
    -        return_value=ReasonerResult(
    -            reply="ok",
    -            tools_used=["tool_search", "x"],
    -        )
    -    )
    -
    -    result = asyncio.run(_run_turn(reasoner, _session()))
    -
    -    assert result.reply == "ok"
    -    assert result.tools_used == ["tool_search", "x"]
    -    assert result.tool_chain == []
    -    assert result.thinking is None
    -    assert result.context_retry["selected_plan"] == "full_context"
    -    assert result.context_retry["trimmed_sections"] == []
    -    assert result.context_retry["attempts"] == [
    -        {
    -            "name": "full_context",
    -            "history_window": 6,
    -            "disabled_sections": [],
    -        }
    -    ]
    -    assert "x" in discovery._unlocked["s:1"]
    -    assert reasoner.run.await_count == 1
    -
    -
    -def test_reasoner_run_turn_context_length_returns_final_user_error():
    -    reasoner = _make_reasoner(discovery=ToolDiscoveryState(), tool_search_enabled=False)
    -    reasoner.run = AsyncMock(side_effect=ContextLengthError("long"))
    -
    -    session = _session()
    -    original_messages = list(session.messages)
    -    result = asyncio.run(_run_turn(reasoner, session))
    -
    -    assert "上下文过长" in str(result.reply)
    -    assert result.tools_used == []
    -    assert result.tool_chain == []
    -    assert result.context_retry["selected_plan"] is None
    -    assert result.context_retry["attempts"] == [
    -        {
    -            "name": "full_context",
    -            "history_window": 6,
    -            "disabled_sections": [],
    -        }
    -    ]
    -    assert reasoner.run.await_count == 1
    -    assert session.messages == original_messages
    -
    -
    -def test_reasoner_run_turn_keeps_full_context_without_dynamic_or_history_trimming():
    -    calls: list[dict[str, object]] = []
    -
    -    def _render(request: ContextRequest, **kwargs: object) -> AssembledTurnInput:
    -        calls.append(
    -            {
    -                "history": list(request.history),
    -                "disabled_sections": set(request.disabled_sections or set()),
    -            }
    -        )
    -        return AssembledTurnInput(
    -            system_prompt="test context",
    -            messages=[
    -                {"role": "system", "content": "test context"},
    -                *list(request.history),
    -                {"role": "user", "content": request.current_message},
    -            ],
    -        )
    -
    -    reasoner = _make_reasoner(
    -        discovery=ToolDiscoveryState(),
    -        tool_search_enabled=False,
    -        render=_render,
    -    )
    -    reasoner.run = AsyncMock(side_effect=ContextLengthError("long"))
    -    session = _session()
    -    result = asyncio.run(_run_turn(reasoner, session))
    -
    -    assert "上下文过长" in str(result.reply)
    -    assert len(calls) == 1
    -    assert calls[0]["history"] == session.messages
    -    assert calls[0]["disabled_sections"] == set()
    -    assert result.context_retry["attempts"] == [
    -        {
    -            "name": "full_context",
    -            "history_window": len(session.messages),
    -            "disabled_sections": [],
    -        }
    -    ]
    diff --git a/tests/test_scheduler_v3_shadow.py b/tests/test_scheduler_v3_shadow.py
    deleted file mode 100644
    index dd85704e8..000000000
    --- a/tests/test_scheduler_v3_shadow.py
    +++ /dev/null
    @@ -1,743 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import ast
    -import shutil
    -from datetime import UTC, datetime, timedelta
    -from dataclasses import replace
    -from types import SimpleNamespace
    -from pathlib import Path
    -from typing import Any, cast
    -
    -import pytest
    -
    -import agent.plugins.manager as plugin_manager_module
    -from agent.control.timer import TimerReceipt, TimerStatus
    -from agent.turn_effects import PostCommitEffect, TurnStorage
    -from agent.control.scoped_turn import TurnAdmissionRetiredError
    -from agent.plugin_composition.channels import ChannelDeliveryReceipt, DeliveryStatus
    -from agent.plugin_composition.deliveries import PluginDeliveries
    -from agent.plugin_composition.scoped_turns import PluginScopedTurns
    -from agent.plugin_composition.timers import PluginTimers
    -from agent.scheduler import (
    -    JobStore,
    -    SCHEDULE_MAX_ACTIVE_JOBS,
    -    ScheduleCapacityError,
    -    ScheduledJob,
    -)
    -from agent.control.models import TurnRequest
    -from agent.control.ports import ControlExecutionResult
    -from agent.control.runtime import ConversationRuntime
    -from agent.plugins.composable import ComposablePlugin
    -from agent.plugins.manager import PluginManager
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from session.store import SessionStore
    -from plugins.scheduler import plugin as scheduler_plugin
    -from plugins.scheduler.plugin import SchedulerRuntime
    -
    -
    -class _TimerHandle:
    -    def __init__(self, timer_id: str, deadline: datetime, now: datetime) -> None:
    -        self._id = timer_id
    -        self.deadline = deadline
    -        self.now = now
    -        self.future: asyncio.Future[TimerReceipt] = (
    -            asyncio.get_running_loop().create_future()
    -        )
    -
    -    @property
    -    def id(self) -> str:
    -        return self._id
    -
    -    async def result(self) -> TimerReceipt:
    -        return await asyncio.shield(self.future)
    -
    -    async def cancel(self) -> TimerReceipt:
    -        if not self.future.done():
    -            self.future.set_result(self._receipt(TimerStatus.CANCELLED))
    -        return await self.future
    -
    -    async def cleanup(self) -> None:
    -        _ = await self.cancel()
    -
    -    def fire(self) -> None:
    -        self.future.set_result(self._receipt(TimerStatus.FIRED))
    -
    -    def _receipt(self, status: TimerStatus) -> TimerReceipt:
    -        return TimerReceipt(self.id, self.deadline, self.now, status)
    -
    -
    -class _Timer:
    -    def __init__(self, now: datetime) -> None:
    -        self.now = now
    -        self.handles: list[_TimerHandle] = []
    -
    -    def schedule(self, deadline: datetime) -> _TimerHandle:
    -        handle = _TimerHandle(f"timer:{len(self.handles)}", deadline, self.now)
    -        self.handles.append(handle)
    -        return handle
    -
    -
    -class _TurnHandle:
    -    def __init__(self, content: str | None, status: str = "completed") -> None:
    -        self._result = SimpleNamespace(
    -            status=SimpleNamespace(value=status),
    -            final_response=content,
    -        )
    -
    -    async def result(self) -> object:
    -        return self._result
    -
    -    async def cleanup(self) -> None:
    -        return None
    -
    -
    -class _Turns:
    -    def __init__(self, response: str | None = "soft result") -> None:
    -        self.response = response
    -        self.sessions: list[tuple[str, dict[str, object]]] = []
    -        self.starts: list[dict[str, object]] = []
    -
    -    async def ensure_session(self, key: str, *, metadata: dict[str, object]) -> str:
    -        self.sessions.append((key, metadata))
    -        return key
    -
    -    async def start(
    -        self, session_id: str, content: str, **kwargs: object
    -    ) -> _TurnHandle:
    -        self.starts.append({"session_id": session_id, "content": content, **kwargs})
    -        return _TurnHandle(self.response)
    -
    -
    -class _RetiredTurns(_Turns):
    -    async def start(
    -        self, session_id: str, content: str, **kwargs: object
    -    ) -> _TurnHandle:
    -        _ = session_id, content, kwargs
    -        raise TurnAdmissionRetiredError("fixture Root retired before admission")
    -
    -
    -async def _settled() -> None:
    -    for _ in range(10):
    -        await asyncio.sleep(0)
    -
    -
    -async def _eventually(predicate) -> None:
    -    for _ in range(100):
    -        if predicate():
    -            return
    -        await asyncio.sleep(0.01)
    -    raise AssertionError("condition did not settle")
    -
    -
    -def _job(
    -    now: datetime,
    -    *,
    -    tier: str = "instant",
    -    trigger: str = "after",
    -    job_id: str = "weather-d494",
    -    fire_at: datetime | None = None,
    -) -> ScheduledJob:
    -    return ScheduledJob(
    -        id=job_id,
    -        trigger=trigger,
    -        tier=tier,
    -        fire_at=fire_at or now + timedelta(seconds=30),
    -        channel="fixture",
    -        chat_id="chat",
    -        interval_seconds=60 if trigger == "every" else None,
    -        message="drink" if tier == "instant" else None,
    -        prompt="weather" if tier == "soft" else None,
    -        timezone="UTC",
    -    )
    -
    -
    -def _runtime(tmp_path, now: datetime, timer: _Timer, turns: _Turns, deliveries):
    -    return SchedulerRuntime(
    -        tmp_path / "schedules.json",
    -        PluginTimers(timer),
    -        cast(PluginScopedTurns, turns),
    -        PluginDeliveries(deliveries),
    -        now=lambda: now,
    -    )
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_one_shot_fires_delivers_and_disables(tmp_path) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -    delivered = []
    -
    -    async def send(message):
    -        delivered.append(message)
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -    await runtime.add_job(_job(now))
    -    timer.handles[0].fire()
    -    await _settled()
    -
    -    stored = runtime.store.load()
    -    assert [message.content for message in delivered] == ["drink"]
    -    assert runtime.wait_count == 0
    -    assert stored[0].enabled is False
    -    assert stored[0].run_count == 1
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_every_settles_then_arms_exactly_one_next_wait(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -    await runtime.add_job(_job(now, trigger="every"))
    -    timer.handles[0].fire()
    -    await _settled()
    -
    -    stored = runtime.store.load()[0]
    -    assert len(timer.handles) == 2
    -    assert runtime.wait_count == 1
    -    assert stored.run_count == 1
    -    assert stored.fire_at > now + timedelta(seconds=30)
    -    await runtime.close()
    -    assert runtime.wait_count == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_delivery_rejection_does_not_count_success(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -
    -    async def reject(_message):
    -        return ChannelDeliveryReceipt(
    -            "delivery:1", DeliveryStatus.REJECTED, error="offline"
    -        )
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), reject)
    -    await runtime.start()
    -    await runtime.add_job(_job(now))
    -    timer.handles[0].fire()
    -    await _settled()
    -
    -    stored = runtime.store.load()[0]
    -    assert stored.enabled is False
    -    assert stored.run_count == 0
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_soft_uses_stateless_memoryless_scoped_turn(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -    turns = _Turns("weather result")
    -    delivered = []
    -
    -    async def send(message):
    -        delivered.append(message)
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, turns, send)
    -    await runtime.start()
    -    await runtime.add_job(_job(now, tier="soft"))
    -    timer.handles[0].fire()
    -    await _settled()
    -
    -    scope = turns.starts[0]["scope"]
    -    assert turns.sessions[0][0] == "scheduler:weather-d494"
    -    assert scope.storage is TurnStorage.IN_MEMORY
    -    assert scope.post_commit_effect is PostCommitEffect.SUPPRESS
    -    assert scope.disabled_prompt_sections == frozenset({"memory"})
    -    assert scope.tool_grant.allows("web_search") is True
    -    assert scope.tool_grant.allows("message_push") is False
    -    assert [message.content for message in delivered] == ["weather result"]
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_hands_unaccepted_soft_job_to_new_root(tmp_path) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    old_timer = _Timer(now)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    old = _runtime(tmp_path, now, old_timer, _RetiredTurns(), send)
    -    await old.start()
    -    await old.add_job(_job(now, tier="soft"))
    -    old_timer.handles[0].fire()
    -    await _settled()
    -
    -    stored = old.store.load()[0]
    -    assert stored.enabled is True
    -    assert stored.run_count == 0
    -    assert old.wait_count == 0
    -
    -    new_timer = _Timer(now)
    -    new = _runtime(tmp_path, now, new_timer, _Turns(), send)
    -    await new.start()
    -    assert new.wait_count == 1
    -    await old.close()
    -    await new.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_cancel_and_dispose_leave_no_wait_or_delivery(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -    delivered = []
    -
    -    async def send(message):
    -        delivered.append(message)
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -    await runtime.add_job(_job(now))
    -
    -    assert await runtime.cancel_job("weather-d494") is True
    -    await runtime.close()
    -
    -    assert runtime.wait_count == 0
    -    assert runtime.store.load() == []
    -    assert delivered == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_capacity_rejects_before_write_or_wait(tmp_path) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -    for index in range(SCHEDULE_MAX_ACTIVE_JOBS):
    -        await runtime.add_job(_job(now, job_id=f"job-{index}"))
    -
    -    before = (len(runtime.store.load()), len(timer.handles), runtime.wait_count)
    -    with pytest.raises(ScheduleCapacityError):
    -        await runtime.add_job(_job(now, job_id="overflow"))
    -
    -    assert before == (
    -        SCHEDULE_MAX_ACTIVE_JOBS,
    -        SCHEDULE_MAX_ACTIVE_JOBS,
    -        SCHEDULE_MAX_ACTIVE_JOBS,
    -    )
    -    assert (len(runtime.store.load()), len(timer.handles), runtime.wait_count) == before
    -    await runtime.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_recovers_grace_expired_every_and_disabled(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    path = tmp_path / "schedules.json"
    -    within = _job(
    -        now,
    -        job_id="within",
    -        fire_at=now - timedelta(seconds=100),
    -    )
    -    expired = _job(
    -        now,
    -        job_id="expired",
    -        fire_at=now - timedelta(seconds=301),
    -    )
    -    every = _job(
    -        now,
    -        trigger="every",
    -        job_id="every",
    -        fire_at=now - timedelta(hours=3),
    -    )
    -    disabled = replace(_job(now, job_id="disabled"), enabled=False)
    -    JobStore(path).save({job.id: job for job in (within, expired, every, disabled)})
    -    timer = _Timer(now)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -
    -    stored = {job.id: job for job in runtime.store.load()}
    -    assert {wait.job.id for wait in runtime._waits.values()} == {"within", "every"}
    -    assert stored["expired"].enabled is False
    -    assert stored["every"].fire_at > now
    -    assert stored["disabled"].enabled is False
    -    await runtime.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_no_work_arms_nothing(tmp_path) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    disabled = replace(_job(now), enabled=False)
    -    JobStore(tmp_path / "schedules.json").save({disabled.id: disabled})
    -    timer = _Timer(now)
    -
    -    async def send(_message):
    -        raise AssertionError("disabled job must not deliver")
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    await runtime.start()
    -    assert runtime.wait_count == 0
    -    assert timer.handles == []
    -    await runtime.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_shadow_restart_arms_one_wait_and_cron_advances(
    -    tmp_path,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    first_timer = _Timer(now)
    -    first = _runtime(tmp_path, now, first_timer, _Turns(), send)
    -    cron = replace(
    -        _job(now, trigger="every", job_id="cron"),
    -        interval_seconds=None,
    -        cron_expr="0 9 * * *",
    -        timezone="Asia/Shanghai",
    -    )
    -    await first.add_job(cron)
    -    await first.close()
    -
    -    second_timer = _Timer(now)
    -    restarted = _runtime(tmp_path, now, second_timer, _Turns(), send)
    -    await restarted.start()
    -    assert restarted.wait_count == 1
    -    assert len(second_timer.handles) == 1
    -    second_timer.handles[0].fire()
    -    await _settled()
    -    stored = restarted.store.load()[0]
    -    assert stored.run_count == 1
    -    assert stored.fire_at > cron.fire_at
    -    assert restarted.wait_count == 1
    -    await restarted.close()
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("soft_response", [None, ""])
    -async def test_scheduler_shadow_soft_terminal_without_content_is_failure(
    -    tmp_path,
    -    soft_response,
    -) -> None:
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -    delivered = []
    -
    -    async def send(message):
    -        delivered.append(message)
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(soft_response), send)
    -    await runtime.start()
    -    await runtime.add_job(_job(now, tier="soft"))
    -    timer.handles[0].fire()
    -    await _settled()
    -
    -    stored = runtime.store.load()[0]
    -    assert stored.enabled is False
    -    assert stored.run_count == 0
    -    assert delivered == []
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_plugin_tools_keep_schema_and_drive_private_runtime(
    -    tmp_path,
    -    monkeypatch,
    -) -> None:
    -    monkeypatch.setenv("TZ", "Asia/Shanghai")
    -    now = datetime(2026, 8, 22, 12, tzinfo=UTC)
    -    timer = _Timer(now)
    -
    -    async def send(_message):
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    runtime = _runtime(tmp_path, now, timer, _Turns(), send)
    -    try:
    -        definitions = {item.name: item for item in scheduler_plugin._tool_definitions()}
    -        assert set(definitions) == {"schedule", "list_schedules", "cancel_schedule"}
    -        assert all(
    -            item.parameters["additionalProperties"] is False
    -            for item in definitions.values()
    -        )
    -        arguments = {
    -            "tier": "instant",
    -            "trigger": "after",
    -            "when": "5m",
    -            "message": "drink",
    -            "channel": "fixture",
    -            "chat_id": "chat",
    -            "request_time": now.isoformat(),
    -            "name": "water",
    -        }
    -        result = await scheduler_plugin._schedule(runtime, object(), arguments)
    -        assert result.startswith("已注册定时任务 「water」")
    -        assert runtime.store.load()[0].timezone == "Asia/Shanghai"
    -        assert "water" in await scheduler_plugin._list_schedules(runtime, object(), {})
    -        assert (
    -            await scheduler_plugin._cancel_schedule(
    -                runtime, object(), {"name": "water"}
    -            )
    -            == "已取消 1 个名为 'water' 的任务"
    -        )
    -        assert runtime.wait_count == 0
    -
    -        monkeypatch.delenv("TZ")
    -        error = await scheduler_plugin._schedule(runtime, object(), arguments)
    -        assert "无效的时区" in error
    -    finally:
    -        await runtime.close()
    -
    -
    -def test_scheduler_plugin_imports_only_public_composition_and_domain_ports() -> None:
    -    source = Path(scheduler_plugin.__file__).read_text(encoding="utf-8")
    -    imported = {
    -        node.module
    -        for node in ast.walk(ast.parse(source))
    -        if isinstance(node, ast.ImportFrom) and node.module is not None
    -    }
    -    forbidden = {
    -        "agent.plugins.manager",
    -        "agent.looping.core",
    -        "agent.tools.registry",
    -        "session.store",
    -        "agent.tools.message_push",
    -    }
    -    assert imported.isdisjoint(forbidden)
    -    assert "SchedulerService" not in source
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_v3_loader_mounts_dormant_and_candidate_never_reads_store(
    -    tmp_path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    schedules = workspace / "schedules.json"
    -    schedules.write_text("not-json", encoding="utf-8")
    -    store = SessionStore(workspace / "sessions.db")
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        raise AssertionError("dormant scheduler must not start a Turn")
    -
    -    async def deliver(_message):
    -        raise AssertionError("dormant scheduler must not deliver")
    -
    -    conversation = ConversationRuntime(store, execute)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(__file__).resolve().parents[1] / "plugins" / "scheduler"],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        conversation,
    -        programmatic_session_creator=store.create_session,
    -        programmatic_session_reader=store.get_session_meta,
    -    )
    -    manager.bind_delivery_sender(deliver)
    -    await manager.load_all()
    -
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    generation = snapshot.generations["scheduler"]
    -    plugin = cast(ComposablePlugin, generation.instance)
    -    assert plugin.workspace_files == ("schedules.json",)
    -    assert snapshot.tool_registry is not None
    -    assert snapshot.tool_registry.get_document("schedule").risk == "write"
    -    assert schedules.read_text(encoding="utf-8") == "not-json"
    -
    -    candidate = await manager.prepare_candidate("scheduler")
    -    assert candidate is not None
    -    assert schedules.read_text(encoding="utf-8") == "not-json"
    -    await manager.discard_prepared("scheduler")
    -    if snapshot.composition_root is not None:
    -        await snapshot.composition_root.dispose()
    -    await conversation.shutdown()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_runtime_lifecycle_follows_hot_reloaded_stable_root(
    -    tmp_path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    source = Path(scheduler_plugin.__file__)
    -    plugin_dir = tmp_path / "plugins" / "scheduler"
    -    plugin_dir.mkdir(parents=True)
    -    shutil.copy2(source, plugin_dir / "plugin.py")
    -    now = datetime.now(UTC)
    -    job = _job(now, fire_at=now + timedelta(hours=1))
    -    JobStore(workspace / "schedules.json").save({job.id: job})
    -    store = SessionStore(workspace / "sessions.db")
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        raise AssertionError("future fixture must not start a Turn")
    -
    -    async def deliver(_message):
    -        raise AssertionError("future fixture must not deliver")
    -
    -    conversation = ConversationRuntime(store, execute)
    -    manager = PluginManager(
    -        plugin_dirs=[plugin_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        conversation,
    -        programmatic_session_creator=store.create_session,
    -        programmatic_session_reader=store.get_session_meta,
    -    )
    -    manager.bind_delivery_sender(deliver)
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -
    -    def active_waits() -> int:
    -        return sum(
    -            task.get_name() == f"scheduler:{job.id}" and not task.done()
    -            for task in asyncio.all_tasks()
    -        )
    -
    -    try:
    -        await _eventually(lambda: active_waits() == 1)
    -        assert manager.current_snapshot.lease_count == 0
    -
    -        with (plugin_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -            handle.write("\n# fixture revision\n")
    -        assert await manager.prepare_candidate("scheduler") is not None
    -        result = await manager.publish_prepared("scheduler")
    -        assert result["publication_state"] == "committed"
    -
    -        await _eventually(lambda: active_waits() == 1)
    -    finally:
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -        await _eventually(lambda: active_waits() == 0)
    -        await conversation.shutdown()
    -        store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_scheduler_hot_reload_hands_unaccepted_job_to_new_root(
    -    tmp_path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    """Prove an old fire cannot mutate the ledger after its Root retires."""
    -
    -    # 1. Mount the real plugin with controllable Core Timer implementations.
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    source = Path(scheduler_plugin.__file__)
    -    plugin_dir = tmp_path / "plugins" / "scheduler"
    -    plugin_dir.mkdir(parents=True)
    -    shutil.copy2(source, plugin_dir / "plugin.py")
    -    now = datetime.now(UTC)
    -    job = _job(now, tier="soft", fire_at=now + timedelta(hours=1))
    -    JobStore(workspace / "schedules.json").save({job.id: job})
    -    timers: list[_Timer] = []
    -
    -    def timer_factory() -> _Timer:
    -        timer = _Timer(now)
    -        timers.append(timer)
    -        return timer
    -
    -    monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", timer_factory)
    -    executions: list[TurnRequest] = []
    -    delivered = []
    -    store = SessionStore(workspace / "sessions.db")
    -
    -    async def execute(request: TurnRequest) -> ControlExecutionResult:
    -        executions.append(request)
    -        return ControlExecutionResult(response="new root result")
    -
    -    async def deliver(message):
    -        delivered.append(message)
    -        return ChannelDeliveryReceipt("delivery:1", DeliveryStatus.DELIVERED)
    -
    -    conversation = ConversationRuntime(store, execute)
    -    manager = PluginManager(
    -        plugin_dirs=[plugin_dir],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        conversation,
    -        programmatic_session_creator=store.create_session,
    -        programmatic_session_reader=store.get_session_meta,
    -    )
    -    manager.bind_delivery_sender(deliver)
    -    await manager.load_all()
    -    lifecycle = asyncio.create_task(manager.run_runtime_services())
    -    release_stop = asyncio.Event()
    -    stop_entered = asyncio.Event()
    -    try:
    -        await _eventually(lambda: sum(bool(timer.handles) for timer in timers) == 1)
    -        old_timer = next(timer for timer in timers if timer.handles)
    -        old_snapshot = manager.current_snapshot
    -        original_stop = cast(Any, manager)._stop_runtime_snapshot
    -
    -        async def gated_stop(snapshot) -> None:
    -            if snapshot is old_snapshot:
    -                stop_entered.set()
    -                await release_stop.wait()
    -            await original_stop(snapshot)
    -
    -        cast(Any, manager)._stop_runtime_snapshot = gated_stop
    -        with (plugin_dir / "plugin.py").open("a", encoding="utf-8") as handle:
    -            handle.write("\n# handoff fixture revision\n")
    -        assert await manager.prepare_candidate("scheduler") is not None
    -        publication = asyncio.create_task(manager.publish_prepared("scheduler"))
    -        await asyncio.wait_for(stop_entered.wait(), timeout=5)
    -        assert not publication.done()
    -
    -        # 2. While release is gated, durable work still belongs to the old Root.
    -        retained = JobStore(workspace / "schedules.json").load()[0]
    -        assert retained.enabled is True
    -        assert retained.run_count == 0
    -        assert executions == []
    -        assert delivered == []
    -
    -        # 3. Release the old Root; cancellation preserves work for the new Root.
    -        release_stop.set()
    -        result = await publication
    -        assert result["publication_state"] == "committed"
    -        await _eventually(lambda: sum(bool(timer.handles) for timer in timers) == 2)
    -        new_timer = next(
    -            timer for timer in timers if timer is not old_timer and timer.handles
    -        )
    -        new_timer.handles[0].fire()
    -        await _eventually(lambda: len(delivered) == 1)
    -        settled = JobStore(workspace / "schedules.json").load()[0]
    -        assert len(executions) == 1
    -        assert delivered[0].content == "new root result"
    -        assert settled.enabled is False
    -        assert settled.run_count == 1
    -    finally:
    -        release_stop.set()
    -        lifecycle.cancel()
    -        _ = await asyncio.gather(lifecycle, return_exceptions=True)
    -        await manager.terminate_all()
    -        await conversation.shutdown()
    -        store.close()
    diff --git a/tests/test_select_akasha_embedding_plugin_migration.py b/tests/test_select_akasha_embedding_plugin_migration.py
    deleted file mode 100644
    index 3f332b8e4..000000000
    --- a/tests/test_select_akasha_embedding_plugin_migration.py
    +++ /dev/null
    @@ -1,265 +0,0 @@
    -import hashlib
    -import importlib.util
    -import json
    -import os
    -import stat
    -import sys
    -import tomllib
    -from pathlib import Path
    -
    -import pytest
    -import yoyo
    -
    -from agent.migrations.context import bind_migration_context
    -
    -_PROJECT_ROOT = Path(__file__).parents[1]
    -_MIGRATION_PATH = (
    -    _PROJECT_ROOT
    -    / "migrations"
    -    / "yoyo"
    -    / "20260825_02_select_akasha_embedding_plugin.py"
    -)
    -
    -
    -def _load_migration():
    -    """Load the migration callback without wrapping it in Yoyo."""
    -
    -    spec = importlib.util.spec_from_file_location(
    -        "select_akasha_embedding_plugin_under_test",
    -        _MIGRATION_PATH,
    -    )
    -    if spec is None or spec.loader is None:
    -        raise RuntimeError(f"无法加载迁移: {_MIGRATION_PATH}")
    -    original_step = yoyo.step
    -    yoyo.step = lambda callback: callback  # type: ignore[assignment]
    -    try:
    -        module = importlib.util.module_from_spec(spec)
    -        sys.modules[spec.name] = module
    -        spec.loader.exec_module(module)
    -    finally:
    -        yoyo.step = original_step
    -    return module
    -
    -
    -def _run(module, config: Path, workspace: Path) -> None:
    -    with bind_migration_context(config_path=config, workspace=workspace):
    -        module.select_akasha_embedding_plugin(None)
    -
    -
    -def _legacy_config(*, engine: str | None = "akasha", extra: str = "") -> bytes:
    -    engine_line = "" if engine is None else f'engine = "{engine}"\n'
    -    return (
    -        "# Preserve operator-owned content.\n"
    -        "[memory]\n"
    -        "enabled = true\n"
    -        f"{engine_line}\n"
    -        "[memory.embedding]\n"
    -        'model = "embedding-model"\n\n'
    -        f"{extra}"
    -        "[custom]\n"
    -        'value = "protected"\n'
    -    ).encode("utf-8")
    -
    -
    -def _backup_root(workspace: Path) -> Path:
    -    roots = sorted((workspace / "backups/select-akasha-embedding-plugin").iterdir())
    -    assert len(roots) == 1
    -    return roots[0]
    -
    -
    -def test_exact_legacy_akasha_selection_moves_to_plugin_claim(
    -    tmp_path: Path,
    -) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    original = _legacy_config()
    -    config.write_bytes(original)
    -    config.chmod(0o640)
    -
    -    _run(module, config, workspace)
    -    _run(module, config, workspace)
    -
    -    migrated = tomllib.loads(config.read_text(encoding="utf-8"))
    -    assert migrated["memory"] == {
    -        "enabled": True,
    -        "embedding": {"model": "embedding-model"},
    -    }
    -    assert migrated["agent"]["plugins"]["disabled_builtin"] == []
    -    assert migrated["custom"] == {"value": "protected"}
    -    assert stat.S_IMODE(config.stat().st_mode) == 0o640
    -
    -    backup_root = _backup_root(workspace)
    -    manifest = json.loads((backup_root / "manifest.json").read_text(encoding="utf-8"))
    -    backup = backup_root / manifest["source"]["backup"]
    -    assert backup.read_bytes() == original
    -    assert manifest["source"]["sha256"] == hashlib.sha256(original).hexdigest()
    -    assert stat.S_IMODE(backup_root.stat().st_mode) == 0o700
    -    assert stat.S_IMODE(backup.stat().st_mode) == 0o600
    -    assert stat.S_IMODE((backup_root / "manifest.json").stat().st_mode) == 0o600
    -
    -
    -def test_existing_plugin_exclusions_are_preserved(tmp_path: Path) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    config.write_bytes(
    -        _legacy_config(
    -            extra=(
    -                "[agent.plugins]\n"
    -                'disabled_builtin = ["scheduler", "default_memory", "akasha"]\n\n'
    -            )
    -        )
    -    )
    -
    -    _run(module, config, workspace)
    -
    -    migrated = tomllib.loads(config.read_text(encoding="utf-8"))
    -    assert migrated["agent"]["plugins"]["disabled_builtin"] == [
    -        "scheduler",
    -    ]
    -
    -
    -@pytest.mark.parametrize(
    -    "engine",
    -    (
    -        None,
    -        "",
    -        "default",
    -        "akasha",
    -    ),
    -)
    -def test_enabled_legacy_memory_choices_select_akasha(
    -    tmp_path: Path,
    -    engine: str | None,
    -) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    config.write_bytes(_legacy_config(engine=engine))
    -
    -    _run(module, config, workspace)
    -
    -    migrated = tomllib.loads(config.read_text(encoding="utf-8"))
    -    assert "engine" not in migrated["memory"]
    -    assert migrated["agent"]["plugins"]["disabled_builtin"] == []
    -
    -
    -def test_disabled_legacy_memory_stays_disabled_without_replay_selection(
    -    tmp_path: Path,
    -) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    legacy_database = workspace / "memory" / "memory2.db"
    -    legacy_database.parent.mkdir()
    -    legacy_database.write_bytes(b"retired-default-memory-archive")
    -    config = tmp_path / "config.toml"
    -    config.write_text(
    -        '[memory]\nenabled = false\nengine = "default"\n',
    -        encoding="utf-8",
    -    )
    -
    -    _run(module, config, workspace)
    -
    -    migrated = tomllib.loads(config.read_text(encoding="utf-8"))
    -    assert migrated["memory"] == {"enabled": False}
    -    assert migrated["agent"]["plugins"]["disabled_builtin"] == ["akasha", "wake"]
    -    assert legacy_database.read_bytes() == b"retired-default-memory-archive"
    -
    -
    -@pytest.mark.parametrize(
    -    "memory",
    -    (
    -        '[memory]\nenabled = true\nengine = "custom"\n',
    -        "[custom]\nvalue = 1\n",
    -    ),
    -)
    -def test_nonmatching_memory_choices_are_noop(tmp_path: Path, memory: str) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    original = memory.encode("utf-8")
    -    config.write_bytes(original)
    -
    -    _run(module, config, workspace)
    -
    -    assert config.read_bytes() == original
    -    assert not (workspace / "backups").exists()
    -
    -
    -@pytest.mark.parametrize(
    -    ("plugins", "message"),
    -    (
    -        (
    -            '[agent.plugins]\ndisabled_builtin = "default_memory"\n',
    -            "必须是合法字符串数组",
    -        ),
    -        (
    -            '[agent.plugins]\ndisabled_builtin = ["scheduler", "scheduler"]\n',
    -            "不允许重复插件名",
    -        ),
    -    ),
    -)
    -def test_conflicting_plugin_configuration_fails_before_write(
    -    tmp_path: Path,
    -    plugins: str,
    -    message: str,
    -) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    original = _legacy_config(extra=f"{plugins}\n")
    -    config.write_bytes(original)
    -
    -    with pytest.raises((RuntimeError, ValueError), match=message):
    -        _run(module, config, workspace)
    -
    -    assert config.read_bytes() == original
    -    assert not (workspace / "backups").exists()
    -
    -
    -def test_symlink_identity_survives_migration(tmp_path: Path) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    target = tmp_path / "config-source.toml"
    -    target.write_bytes(_legacy_config())
    -    config = tmp_path / "config.toml"
    -    config.symlink_to(target.name)
    -    original_link = os.readlink(config)
    -
    -    _run(module, config, workspace)
    -
    -    assert config.is_symlink()
    -    assert os.readlink(config) == original_link
    -    assert "engine" not in tomllib.loads(target.read_text(encoding="utf-8"))["memory"]
    -
    -
    -def test_failed_publication_restores_exact_config(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    module = _load_migration()
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    config = tmp_path / "config.toml"
    -    original = _legacy_config()
    -    config.write_bytes(original)
    -
    -    def fail_after_write(snapshot, rendered):
    -        module._write_atomic(snapshot.resolved_target, rendered, snapshot.mode)
    -        raise RuntimeError("forced publication failure")
    -
    -    monkeypatch.setattr(module, "_publish_config", fail_after_write)
    -    with pytest.raises(RuntimeError, match="forced publication failure"):
    -        _run(module, config, workspace)
    -
    -    assert config.read_bytes() == original
    -    backup_root = _backup_root(workspace)
    -    assert (backup_root / "config.toml.before").read_bytes() == original
    diff --git a/tests/test_session_attachments.py b/tests/test_session_attachments.py
    deleted file mode 100644
    index 029881187..000000000
    --- a/tests/test_session_attachments.py
    +++ /dev/null
    @@ -1,220 +0,0 @@
    -from __future__ import annotations
    -
    -from datetime import UTC, datetime
    -import sqlite3
    -
    -import pytest
    -
    -from session.store import SessionStore
    -
    -
    -NOW = datetime(2026, 8, 17, 6, 0, tzinfo=UTC).isoformat()
    -ARTIFACT_ID = "a" * 32
    -
    -
    -def _register(store: SessionStore, artifact_id: str = ARTIFACT_ID) -> None:
    -    store.begin_attachment_import(
    -        artifact_id=artifact_id,
    -        storage_key=f"uploads/artifacts/{artifact_id}.bin",
    -        expected_size_bytes=4,
    -        expected_sha256="b" * 64,
    -        created_at=NOW,
    -    )
    -    store.mark_attachment_import_file_published(artifact_id, updated_at=NOW)
    -    store.register_ready_attachment(
    -        artifact_id=artifact_id,
    -        storage_key=f"uploads/artifacts/{artifact_id}.bin",
    -        kind="file",
    -        filename="report.bin",
    -        media_type="application/octet-stream",
    -        size_bytes=4,
    -        sha256="b" * 64,
    -        created_at=NOW,
    -    )
    -
    -
    -def test_message_and_attachment_binding_commit_atomically(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _register(store)
    -
    -    rows = store.persist_session(
    -        "telegram:one",
    -        created_at=NOW,
    -        updated_at=NOW,
    -        metadata={},
    -        messages=[
    -            {
    -                "role": "user",
    -                "content": "file",
    -                "timestamp": NOW,
    -                "tool_chain": None,
    -                "extra": {"attachment_ids": [ARTIFACT_ID]},
    -            }
    -        ],
    -    )
    -
    -    assert [row["id"] for row in rows] == ["telegram:one:0"]
    -    assert store.message_attachment_ids("telegram:one:0") == (ARTIFACT_ID,)
    -    assert store.get_attachment(ARTIFACT_ID) is not None
    -    report = store.validate_attachment_metadata_integrity()
    -    assert report.artifact_count == 1
    -    assert report.binding_count == 1
    -    assert report.bound_message_count == 1
    -    assert report.incomplete_import_ids == ()
    -    store.close()
    -
    -
    -def test_missing_attachment_rolls_back_session_and_message(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -
    -    with pytest.raises(ValueError, match="未发布"):
    -        store.persist_session(
    -            "telegram:missing",
    -            created_at=NOW,
    -            updated_at=NOW,
    -            metadata={"before": True},
    -            messages=[
    -                {
    -                    "role": "user",
    -                    "content": "missing",
    -                    "timestamp": NOW,
    -                    "tool_chain": None,
    -                    "extra": {"attachment_ids": ["missing"]},
    -                }
    -            ],
    -        )
    -
    -    assert not store.session_exists("telegram:missing")
    -    assert store.count_messages("telegram:missing") == 0
    -    assert store.message_attachment_ids("telegram:missing:0") == ()
    -    store.close()
    -
    -
    -def test_explicit_message_delete_removes_binding_but_retains_artifact(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.create_session(key="telegram:delete")
    -    _register(store)
    -    message = store.insert_message(
    -        "telegram:delete",
    -        role="user",
    -        content="delete binding only",
    -        ts=NOW,
    -        seq=0,
    -        extra={"attachment_ids": [ARTIFACT_ID]},
    -    )
    -
    -    assert store.delete_message(
    -        str(message["id"]),
    -        action_source="test.attachment_binding_delete",
    -    )
    -    assert store.message_attachment_ids(str(message["id"])) == ()
    -    assert store.get_attachment(ARTIFACT_ID) is not None
    -    store.close()
    -
    -
    -def test_attachment_identity_and_message_order_are_fail_loud(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    _register(store)
    -
    -    with pytest.raises(ValueError, match="不得重复"):
    -        store.persist_session(
    -            "telegram:duplicate",
    -            created_at=NOW,
    -            updated_at=NOW,
    -            metadata={},
    -            messages=[
    -                {
    -                    "role": "user",
    -                    "content": "duplicate",
    -                    "timestamp": NOW,
    -                    "tool_chain": None,
    -                    "extra": {"attachment_ids": [ARTIFACT_ID, ARTIFACT_ID]},
    -                }
    -            ],
    -        )
    -
    -    assert not store.session_exists("telegram:duplicate")
    -    store.close()
    -
    -
    -def test_binding_foreign_keys_and_message_edit_cannot_drift(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.create_session(key="telegram:edit")
    -    _register(store)
    -    message = store.insert_message(
    -        "telegram:edit",
    -        role="user",
    -        content="original",
    -        ts=NOW,
    -        seq=0,
    -        extra={"attachment_ids": [ARTIFACT_ID]},
    -    )
    -    message_id = str(message["id"])
    -
    -    with pytest.raises(ValueError, match="不允许由 message_edit 改写"):
    -        store.update_message(
    -            message_id,
    -            extra={"attachment_ids": []},
    -            action_source="test.attachment_drift",
    -        )
    -    assert store.message_attachment_ids(message_id) == (ARTIFACT_ID,)
    -
    -    with store._lock, pytest.raises(sqlite3.IntegrityError):
    -        store._conn.execute(
    -            """
    -            INSERT INTO message_attachments (
    -                message_id, ordinal, artifact_id, direction
    -            ) VALUES ('missing-message', 0, ?, 'inbound')
    -            """,
    -            (ARTIFACT_ID,),
    -        )
    -    with store._lock:
    -        store._conn.rollback()
    -    store.close()
    -
    -
    -def test_session_cascade_removes_bindings_without_deleting_artifact(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.create_session(key="telegram:cascade")
    -    _register(store)
    -    message = store.insert_message(
    -        "telegram:cascade",
    -        role="user",
    -        content="session delete",
    -        ts=NOW,
    -        seq=0,
    -        extra={"attachment_ids": [ARTIFACT_ID]},
    -    )
    -
    -    assert store.delete_session(
    -        "telegram:cascade",
    -        cascade=True,
    -        action_source="test.attachment_session_delete",
    -    )
    -    assert store.message_attachment_ids(str(message["id"])) == ()
    -    assert store.get_attachment(ARTIFACT_ID) is not None
    -    store.close()
    -
    -
    -def test_integrity_gate_rejects_projection_drift(tmp_path) -> None:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.create_session(key="telegram:corrupt")
    -    _register(store)
    -    message = store.insert_message(
    -        "telegram:corrupt",
    -        role="user",
    -        content="corrupt only through raw SQL",
    -        ts=NOW,
    -        seq=0,
    -        extra={"attachment_ids": [ARTIFACT_ID]},
    -    )
    -    with store._lock:
    -        store._conn.execute(
    -            "UPDATE messages SET extra = '{}' WHERE id = ?",
    -            (str(message["id"]),),
    -        )
    -        store._conn.commit()
    -
    -    with pytest.raises(ValueError, match="projection 已漂移"):
    -        store.validate_attachment_metadata_integrity()
    -    store.close()
    diff --git a/tests/test_session_compaction_source_plan_digest.py b/tests/test_session_compaction_source_plan_digest.py
    deleted file mode 100644
    index 4784ec5ba..000000000
    --- a/tests/test_session_compaction_source_plan_digest.py
    +++ /dev/null
    @@ -1,103 +0,0 @@
    -from __future__ import annotations
    -
    -import pytest
    -from typing import Any, TypedDict
    -
    -from session.store import SessionStore
    -
    -
    -class _CompactionKwargs(TypedDict):
    -    session_key: str
    -    trigger: str
    -    summary: str
    -    source_ref: str
    -    source_plan_digest: str
    -    source_from_seq: int
    -    consolidated_through_seq: int
    -    source_message_ids: list[str]
    -    retained_tail: list[dict[str, Any]]
    -    model_runtime_id: str
    -    model: str
    -    context_window: int
    -    threshold_tokens: int
    -    hard_input_tokens: int
    -    keep_recent_tokens: int
    -    tokens_before: int
    -    tokens_after: int
    -    summary_usage: dict[str, Any]
    -    generation: int
    -    parent_generation: int
    -
    -
    -def _persist_kwargs(session_key: str, digest: str) -> _CompactionKwargs:
    -    return {
    -        "session_key": session_key,
    -        "trigger": "soft_limit",
    -        "summary": "summary",
    -        "source_ref": "source:1",
    -        "source_plan_digest": digest,
    -        "source_from_seq": 0,
    -        "consolidated_through_seq": 0,
    -        "source_message_ids": ["session:digest:0"],
    -        "retained_tail": [
    -            {
    -                "id": "session:digest:0",
    -                "seq": 0,
    -                "unit_ref": "unit:1",
    -                "message": {"role": "user", "content": "source"},
    -            }
    -        ],
    -        "model_runtime_id": "runtime",
    -        "model": "model",
    -        "context_window": 100,
    -        "threshold_tokens": 74,
    -        "hard_input_tokens": 90,
    -        "keep_recent_tokens": 20,
    -        "tokens_before": 80,
    -        "tokens_after": 40,
    -        "summary_usage": {},
    -        "generation": 1,
    -        "parent_generation": 0,
    -    }
    -
    -
    -def _store(tmp_path) -> SessionStore:
    -    store = SessionStore(tmp_path / "sessions.db")
    -    store.create_session(key="session:digest")
    -    store.insert_message(
    -        "session:digest",
    -        role="user",
    -        content="source",
    -        ts="2026-08-08T00:00:00+00:00",
    -        seq=0,
    -    )
    -    return store
    -
    -
    -def test_store_persists_and_reloads_canonical_digest(tmp_path) -> None:
    -    store = _store(tmp_path)
    -    try:
    -        digest = "a" * 64
    -        row = store.persist_compaction(**_persist_kwargs("session:digest", digest))
    -        assert row.source_plan_digest == digest
    -        reopened = SessionStore(tmp_path / "sessions.db")
    -        try:
    -            loaded = reopened.get_compaction("session:digest", 1)
    -            assert loaded is not None
    -            assert loaded.source_plan_digest == digest
    -        finally:
    -            reopened.close()
    -    finally:
    -        store.close()
    -
    -
    -@pytest.mark.parametrize("digest", ("a" * 63, "g" * 64, ""))
    -def test_store_rejects_noncanonical_digest_at_write_boundary(tmp_path, digest: str) -> None:
    -    store = _store(tmp_path)
    -    try:
    -        with pytest.raises(ValueError, match="source_plan_digest"):
    -            store.persist_compaction(**_persist_kwargs("session:digest", digest))
    -        assert store.list_compactions("session:digest") == []
    -        assert store.get_compaction_head("session:digest").parent_generation == 0
    -    finally:
    -        store.close()
    diff --git a/tests/test_session_model_selection.py b/tests/test_session_model_selection.py
    deleted file mode 100644
    index 1977c9a48..000000000
    --- a/tests/test_session_model_selection.py
    +++ /dev/null
    @@ -1,51 +0,0 @@
    -from __future__ import annotations
    -
    -import pytest
    -
    -from agent.model_runtime.session_selection import (
    -    SessionModelSelection,
    -    read_session_model_selection,
    -    write_session_model_selection,
    -)
    -
    -
    -def test_structured_selection_round_trips_model_and_effort() -> None:
    -    metadata: dict[str, object] = {}
    -    write_session_model_selection(
    -        metadata,
    -        SessionModelSelection("deepseek-main", "high"),
    -    )
    -
    -    assert read_session_model_selection(metadata) == SessionModelSelection(
    -        "deepseek-main",
    -        "high",
    -    )
    -
    -
    -def test_legacy_override_remains_readable_until_next_explicit_change() -> None:
    -    metadata: dict[str, object] = {"model_runtime_override": "legacy-main"}
    -    assert read_session_model_selection(metadata) == SessionModelSelection(
    -        "legacy-main",
    -        "",
    -    )
    -
    -    write_session_model_selection(metadata, SessionModelSelection("next", "low"))
    -    assert "model_runtime_override" not in metadata
    -    assert read_session_model_selection(metadata) == SessionModelSelection(
    -        "next",
    -        "low",
    -    )
    -
    -
    -def test_follow_default_removes_pinned_selection() -> None:
    -    metadata: dict[str, object] = {}
    -    write_session_model_selection(metadata, SessionModelSelection("pinned", "high"))
    -
    -    write_session_model_selection(metadata, SessionModelSelection())
    -
    -    assert metadata == {}
    -
    -
    -def test_effort_without_explicit_model_is_rejected() -> None:
    -    with pytest.raises(ValueError, match="默认模型不能单独覆盖推理强度"):
    -        write_session_model_selection({}, SessionModelSelection("", "high"))
    diff --git a/tests/test_setup_wizard.py b/tests/test_setup_wizard.py
    deleted file mode 100644
    index 6a3c88048..000000000
    --- a/tests/test_setup_wizard.py
    +++ /dev/null
    @@ -1,93 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import sys
    -import threading
    -import types
    -
    -import pytest
    -
    -from bootstrap.setup_wizard import (
    -    WizardAnswers,
    -    _async_fetch_qqbot_openid,
    -    _render_config,
    -)
    -
    -
    -def test_setup_wizard_renders_web_chat_config() -> None:
    -    text = _render_config(WizardAnswers())
    -
    -    assert "[channels.chat]" in text
    -    assert "enabled = true" in text
    -    assert "channel_name" not in text
    -
    -
    -def test_setup_wizard_uses_plugin_activation_without_memory_engine_selector() -> None:
    -    text = _render_config(WizardAnswers())
    -
    -    assert "[memory]" not in text
    -    assert "[llm]" not in text
    -    assert "memory.engine" not in text
    -    assert "engine =" not in text
    -    assert "6322" not in text
    -    assert "channel_name" not in text
    -
    -
    -def test_setup_wizard_leaves_model_ownership_to_model_plugin() -> None:
    -    text = _render_config(WizardAnswers())
    -
    -    assert "[llm]" not in text
    -    assert "max_tokens" not in text
    -
    -
    -@pytest.mark.asyncio
    -async def test_qqbot_openid_fetch_times_out_without_ws_frames(
    -    monkeypatch: pytest.MonkeyPatch,
    -) -> None:
    -    class _Resp:
    -        def __init__(self, payload: dict[str, str]) -> None:
    -            self._payload = payload
    -
    -        def json(self) -> dict[str, str]:
    -            return self._payload
    -
    -    class _Client:
    -        def __init__(self, timeout: int) -> None:
    -            self.timeout = timeout
    -
    -        async def __aenter__(self):
    -            return self
    -
    -        async def __aexit__(self, exc_type, exc, tb) -> None:
    -            return None
    -
    -        async def post(self, *args, **kwargs) -> _Resp:
    -            return _Resp({"access_token": "token"})
    -
    -        async def get(self, *args, **kwargs) -> _Resp:
    -            return _Resp({"url": "wss://example.invalid"})
    -
    -    class _NeverFrames:
    -        async def __aenter__(self):
    -            return self
    -
    -        async def __aexit__(self, exc_type, exc, tb) -> None:
    -            return None
    -
    -        def __aiter__(self):
    -            return self
    -
    -        async def __anext__(self):
    -            await asyncio.sleep(3600)
    -            raise StopAsyncIteration
    -
    -    fake_httpx = types.ModuleType("httpx")
    -    fake_httpx.AsyncClient = _Client
    -    fake_websockets = types.ModuleType("websockets")
    -    fake_websockets.connect = lambda *_args, **_kwargs: _NeverFrames()
    -    monkeypatch.setitem(sys.modules, "httpx", fake_httpx)
    -    monkeypatch.setitem(sys.modules, "websockets", fake_websockets)
    -
    -    result = await _async_fetch_qqbot_openid("app", "secret", 1, threading.Event())
    -
    -    assert result is None
    diff --git a/tests/test_skills_loader.py b/tests/test_skills_loader.py
    deleted file mode 100644
    index 889c7bdf1..000000000
    --- a/tests/test_skills_loader.py
    +++ /dev/null
    @@ -1,294 +0,0 @@
    -from pathlib import Path
    -from types import SimpleNamespace
    -
    -import pytest
    -
    -from agent.skills import SkillsLoader
    -from agent.tools.base import ToolResult
    -from agent.tools.skill_loader import LoadSkillTool
    -
    -
    -def _write_skill(
    -    skills_dir: Path,
    -    name: str,
    -    *,
    -    description: str = "测试技能",
    -    body: str = "正文",
    -    extra_frontmatter: str = "",
    -) -> Path:
    -    skill_dir = skills_dir / name
    -    skill_dir.mkdir(parents=True)
    -    extra = f"{extra_frontmatter}\n" if extra_frontmatter else ""
    -    (skill_dir / "SKILL.md").write_text(
    -        f"---\n"
    -        f"name: {name}\n"
    -        f"description: {description}\n"
    -        f"{extra}"
    -        f"---\n"
    -        f"{body}\n",
    -        encoding="utf-8",
    -    )
    -    return skill_dir
    -
    -
    -def test_skill_index_prefers_workspace_over_builtin(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    builtin = tmp_path / "builtin"
    -    _write_skill(builtin, "memory", description="builtin", body="builtin body")
    -    _write_skill(
    -        workspace / "skills",
    -        "memory",
    -        description="workspace",
    -        body="workspace body",
    -    )
    -
    -    loader = SkillsLoader(workspace, builtin_skills_dir=builtin)
    -
    -    records = loader.list_skill_records(filter_unavailable=False)
    -    assert [record.name for record in records] == ["memory"]
    -    assert records[0].source == "workspace"
    -    assert loader.load_skill_body("memory") == "workspace body"
    -
    -
    -def test_skills_summary_hides_file_locations(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    skill_dir = _write_skill(
    -        workspace / "skills",
    -        "memory",
    -        description="处理记忆任务时使用。",
    -        body="body",
    -        extra_frontmatter="when_to_use: 用户询问记忆时。",
    -    )
    -
    -    summary = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin").build_skills_summary()
    -
    -    assert '' in summary
    -    assert "用户询问记忆时。" in summary
    -    assert "" not in summary
    -    assert str(skill_dir / "SKILL.md") not in summary
    -
    -
    -def test_skill_frontmatter_uses_yaml_parser(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    _write_skill(
    -        workspace / "skills",
    -        "memory",
    -        description="处理记忆任务时使用。",
    -        body="body",
    -        extra_frontmatter=(
    -            "when_to_use: |\n"
    -            "  用户询问记忆时。\n"
    -            "metadata:\n"
    -            "  akashic:\n"
    -            "    always: true"
    -        ),
    -    )
    -
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -    record = loader.list_skill_records()[0]
    -
    -    assert record.when_to_use == "用户询问记忆时。\n"
    -    assert record.always is True
    -
    -
    -@pytest.mark.parametrize("metadata", ["metadata:", "metadata: ''"])
    -def test_skill_index_allows_empty_metadata(tmp_path: Path, metadata: str):
    -    workspace = tmp_path / "workspace"
    -    _write_skill(
    -        workspace / "skills",
    -        "empty",
    -        extra_frontmatter=metadata,
    -    )
    -
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -
    -    assert loader.build_index().records["empty"].config == {}
    -
    -
    -def test_skill_index_rejects_invalid_metadata_json(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    skill_dir = _write_skill(
    -        workspace / "skills",
    -        "broken",
    -        extra_frontmatter="metadata: '{broken'",
    -    )
    -
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -
    -    with pytest.raises(ValueError, match="Skill metadata 不是有效 JSON") as exc_info:
    -        loader.build_index()
    -
    -    assert str(skill_dir / "SKILL.md") in str(exc_info.value)
    -
    -
    -@pytest.mark.parametrize("metadata", ["'[]'", "'null'"])
    -def test_skill_index_rejects_non_object_metadata_json(
    -    tmp_path: Path,
    -    metadata: str,
    -):
    -    workspace = tmp_path / "workspace"
    -    skill_dir = _write_skill(
    -        workspace / "skills",
    -        "broken",
    -        extra_frontmatter=f"metadata: {metadata}",
    -    )
    -
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -
    -    with pytest.raises(ValueError, match="Skill metadata 必须是对象") as exc_info:
    -        loader.build_index()
    -
    -    assert str(skill_dir / "SKILL.md") in str(exc_info.value)
    -
    -
    -def test_skill_binary_requirement_uses_user_login_shell_path(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    bin_dir = tmp_path / "user-bin"
    -    bin_dir.mkdir()
    -    executable = bin_dir / "opencli-test"
    -    executable.write_text("#!/bin/sh\n", encoding="utf-8")
    -    executable.chmod(0o755)
    -    _write_skill(
    -        workspace / "skills",
    -        "opencli",
    -        extra_frontmatter=(
    -            'metadata: {"akashic": {"requires": '
    -            '{"bins": ["opencli-test"]}}}'
    -        ),
    -    )
    -    monkeypatch.setenv("PATH", "/usr/bin")
    -    monkeypatch.setattr("agent.skills._default_shell_path", lambda: str(bin_dir))
    -
    -    record = SkillsLoader(
    -        workspace,
    -        builtin_skills_dir=tmp_path / "builtin",
    -    ).build_index().records["opencli"]
    -
    -    assert record.available is True
    -    assert record.missing == ""
    -
    -
    -@pytest.mark.asyncio
    -async def test_load_skill_tool_returns_body_and_base_directory(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    skill_dir = _write_skill(
    -        workspace / "skills",
    -        "memory",
    -        description="处理记忆任务时使用。",
    -        body="读取 guides/intro.md。",
    -    )
    -    tool = LoadSkillTool(SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin"))
    -
    -    result = await tool.execute(skill="memory")
    -
    -    assert isinstance(result, str)
    -    assert "# Skill: memory" in result
    -    assert f"Base directory: {skill_dir.resolve()}" in result
    -    assert "读取 guides/intro.md。" in result
    -    assert "description:" not in result
    -
    -
    -@pytest.mark.asyncio
    -async def test_load_plugin_skill_returns_runtime_owned_provenance(
    -    tmp_path: Path,
    -    monkeypatch: pytest.MonkeyPatch,
    -):
    -    workspace = tmp_path / "workspace"
    -    plugin_skills = tmp_path / "plugin-skills"
    -    _write_skill(plugin_skills, "opencli", body="candidate body")
    -    monkeypatch.setattr(
    -        "agent.plugins.snapshot.get_current_runtime_snapshot",
    -        lambda: SimpleNamespace(
    -            snapshot_id="snapshot-latest",
    -            skill_catalog_generation_id="catalog-candidate",
    -        ),
    -    )
    -    tool = LoadSkillTool(
    -        SkillsLoader(
    -            workspace,
    -            builtin_skills_dir=None,
    -            plugin_roots={"huayue-skills@github": (plugin_skills,)},
    -        )
    -    )
    -
    -    result = await tool.execute(skill="opencli")
    -
    -    assert isinstance(result, ToolResult)
    -    assert "candidate body" in result.text
    -    assert result.runtime_provenance == {
    -        "kind": "plugin-skill",
    -        "skillName": "opencli",
    -        "pluginId": "huayue-skills@github",
    -        "skillCatalogGenerationId": "catalog-candidate",
    -        "runtimeSnapshotId": "snapshot-latest",
    -    }
    -
    -
    -@pytest.mark.asyncio
    -async def test_load_skill_tool_blocks_unavailable_skill(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    _write_skill(
    -        workspace / "skills",
    -        "needs-bin",
    -        body="hidden body",
    -        extra_frontmatter=(
    -            'metadata: {"akashic": {"requires": '
    -            '{"bins": ["definitely-missing-akashic-test-bin"]}}}'
    -        ),
    -    )
    -    tool = LoadSkillTool(SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin"))
    -
    -    result = await tool.execute(skill="needs-bin")
    -
    -    assert isinstance(result, str)
    -    assert "skill 不可用" in result
    -    assert "definitely-missing-akashic-test-bin" in result
    -    assert "hidden body" not in result
    -
    -
    -def test_always_skill_still_loads_into_context(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    _write_skill(
    -        workspace / "skills",
    -        "memory",
    -        body="always body",
    -        extra_frontmatter='metadata: {"akashic": {"always": true}}',
    -    )
    -    loader = SkillsLoader(workspace, builtin_skills_dir=tmp_path / "builtin")
    -
    -    assert loader.get_always_skills() == ["memory"]
    -    assert "always body" in loader.load_skills_for_context(["memory"])
    -
    -
    -def test_plugin_roots_do_not_depend_on_workspace_symlinks(tmp_path: Path):
    -    workspace = tmp_path / "workspace"
    -    plugin_root = tmp_path / "plugin-skills"
    -    skill_dir = _write_skill(plugin_root, "plugin-skill", body="plugin body")
    -    workspace_skills = workspace / "skills"
    -    workspace_skills.mkdir(parents=True)
    -    (workspace_skills / "legacy-link").symlink_to(skill_dir, target_is_directory=True)
    -    personal_target = _write_skill(
    -        tmp_path / "personal-skills",
    -        "personal-target",
    -        body="personal body",
    -    )
    -    (workspace_skills / "personal-link").symlink_to(
    -        personal_target,
    -        target_is_directory=True,
    -    )
    -
    -    loader = SkillsLoader(
    -        workspace,
    -        builtin_skills_dir=None,
    -        plugin_roots={"demo": (plugin_root,)},
    -        ignored_workspace_symlink_roots=(plugin_root,),
    -    )
    -
    -    records = loader.build_index().records
    -    assert set(records) == {"personal-link", "plugin-skill"}
    -    assert records["personal-link"].source == "workspace"
    -    assert records["plugin-skill"].source == "plugin"
    -    assert records["plugin-skill"].source_id == "demo"
    diff --git a/tests/test_stop_script.py b/tests/test_stop_script.py
    deleted file mode 100644
    index 206196065..000000000
    --- a/tests/test_stop_script.py
    +++ /dev/null
    @@ -1,140 +0,0 @@
    -from __future__ import annotations
    -
    -import os
    -import subprocess
    -import sys
    -import time
    -from pathlib import Path
    -
    -
    -PROJECT_ROOT = Path(__file__).resolve().parents[1]
    -STOP_SCRIPT = PROJECT_ROOT / "scripts" / "stop-runtime.sh"
    -LOCK_HOLDER_CODE = """
    -import fcntl
    -import signal
    -import sys
    -
    -stream = open(sys.argv[1], "a+")
    -fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
    -signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))
    -signal.pause()
    -"""
    -SUPERVISOR_CODE = f"""
    -import fcntl
    -import signal
    -import subprocess
    -import sys
    -
    -stream = open(sys.argv[1], "a+")
    -fcntl.flock(stream.fileno(), fcntl.LOCK_EX)
    -child = subprocess.Popen([sys.executable, "-c", {LOCK_HOLDER_CODE!r}, sys.argv[2]])
    -
    -def stop(*_args):
    -    child.terminate()
    -    child.wait(timeout=5)
    -    raise SystemExit(0)
    -
    -signal.signal(signal.SIGTERM, stop)
    -child.wait()
    -"""
    -
    -
    -def _wait_until_locked(lock_path: Path, process: subprocess.Popen[str]) -> None:
    -    deadline = time.monotonic() + 5
    -    while time.monotonic() < deadline:
    -        if process.poll() is not None:
    -            raise AssertionError("fixture process exited before acquiring its lock")
    -        probe = subprocess.run(
    -            ["flock", "-n", str(lock_path), "-c", "true"],
    -            check=False,
    -        )
    -        if probe.returncode != 0:
    -            return
    -        time.sleep(0.05)
    -    process.terminate()
    -    process.wait(timeout=5)
    -    raise AssertionError("fixture process did not acquire its lock")
    -
    -
    -def _start_lock_holder(lock_path: Path) -> subprocess.Popen[str]:
    -    lock_path.parent.mkdir(parents=True)
    -    process = subprocess.Popen(
    -        [sys.executable, "-c", LOCK_HOLDER_CODE, str(lock_path)],
    -        text=True,
    -    )
    -    _wait_until_locked(lock_path, process)
    -    return process
    -
    -
    -def test_stops_runtime_lock_owner(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    process = _start_lock_holder(workspace / ".instance.lock")
    -    try:
    -        result = subprocess.run(
    -            [str(STOP_SCRIPT), "--workspace", str(workspace)],
    -            cwd=PROJECT_ROOT,
    -            check=False,
    -            capture_output=True,
    -            text=True,
    -        )
    -        assert result.returncode == 0, result.stderr
    -        assert "workspace 已停止" in result.stdout
    -        process.wait(timeout=5)
    -    finally:
    -        if process.poll() is None:
    -            process.terminate()
    -            process.wait(timeout=5)
    -
    -
    -def test_keeps_stale_lock_file(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    lock_path = workspace / ".instance.lock"
    -    lock_path.write_text("stale-owner", encoding="utf-8")
    -
    -    result = subprocess.run(
    -        [str(STOP_SCRIPT), "--workspace", str(workspace)],
    -        cwd=PROJECT_ROOT,
    -        check=False,
    -        capture_output=True,
    -        text=True,
    -        env={**os.environ, "AKASHIC_WORKSPACE": ""},
    -    )
    -
    -    assert result.returncode == 0, result.stderr
    -    assert "workspace 未运行" in result.stdout
    -    assert lock_path.read_text(encoding="utf-8") == "stale-owner"
    -
    -
    -def test_stops_supervisor_before_runtime_child(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    supervisor_lock = workspace / ".supervisor.lock"
    -    runtime_lock = workspace / ".instance.lock"
    -    process = subprocess.Popen(
    -        [
    -            sys.executable,
    -            "-c",
    -            SUPERVISOR_CODE,
    -            str(supervisor_lock),
    -            str(runtime_lock),
    -        ],
    -        text=True,
    -    )
    -    try:
    -        _wait_until_locked(supervisor_lock, process)
    -        _wait_until_locked(runtime_lock, process)
    -        result = subprocess.run(
    -            [str(STOP_SCRIPT), "--workspace", str(workspace)],
    -            cwd=PROJECT_ROOT,
    -            check=False,
    -            capture_output=True,
    -            text=True,
    -        )
    -        assert result.returncode == 0, result.stderr
    -        assert f"pid={process.pid}" in result.stdout
    -        process.wait(timeout=5)
    -    finally:
    -        if process.poll() is None:
    -            process.terminate()
    -            process.wait(timeout=5)
    diff --git a/tests/test_structured_logging.py b/tests/test_structured_logging.py
    deleted file mode 100644
    index 88c453df4..000000000
    --- a/tests/test_structured_logging.py
    +++ /dev/null
    @@ -1,344 +0,0 @@
    -from __future__ import annotations
    -
    -import json
    -import logging
    -from datetime import datetime, timedelta
    -from pathlib import Path
    -from typing import Any, cast
    -
    -import pytest
    -
    -from agent.control.models import TurnRequest
    -from agent.control.runtime import ConversationRuntime
    -from bus.event_bus import EventBus
    -from bus.events import InboundMessage, OutboundMessage
    -from bus.events_lifecycle import TurnCommitted
    -from agent.core.passive_turn import _turn_log_id
    -from core.common.diagnostic_log import AkashicJsonFormatter
    -from core.common.diagnostic_log import configure_logging
    -from core.common.diagnostic_log import diagnostic_context
    -from core.common.diagnostic_log import diagnostic_line
    -from core.common.diagnostic_log import log_event
    -from core.common.diagnostic_log import turn_milestone
    -from session.store import SessionStore
    -
    -
    -def test_json_logging_emits_joinable_bounded_event(
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_LOG_FORMAT", "json")
    -    monkeypatch.setenv("AKASHIC_SERVICE_NAME", "akashic-test")
    -    monkeypatch.setenv("AKASHIC_BOOT_ID", "boot-1")
    -    configure_logging()
    -
    -    with diagnostic_context(
    -        session="session-1",
    -        turn="turn-1",
    -        request_id="request-1",
    -    ):
    -        log_event(
    -            logging.getLogger("test.observability"),
    -            logging.INFO,
    -            "test.completed",
    -            content_fp="content-123",
    -            duration_ms=12,
    -            outcome="completed",
    -            message="Authorization: Bearer-secret token=private-value",
    -        )
    -
    -    document = json.loads(capsys.readouterr().err)
    -    assert datetime.fromisoformat(document["timestamp"]).utcoffset() == timedelta(0)
    -    assert document["service"] == "akashic-test"
    -    assert document["event"] == "test.completed"
    -    assert document["session"] == "session-1"
    -    assert document["turn"] == "turn-1"
    -    assert document["request_id"] == "request-1"
    -    assert document["boot_id"] == "boot-1"
    -    assert document["duration_ms"] == 12
    -    assert document["content_fp"] == "content-123"
    -    assert "Bearer-secret" not in document["message"]
    -    assert "private-value" not in document["message"]
    -
    -
    -def test_json_logging_uses_library_formatter_and_drops_arbitrary_extra(
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_LOG_FORMAT", "json")
    -    configure_logging()
    -
    -    handler = logging.getLogger().handlers[0]
    -    assert isinstance(handler.formatter, AkashicJsonFormatter)
    -
    -    logging.getLogger("test.observability").info(
    -        "bounded",
    -        extra={"arbitrary_payload": "must not be logged"},
    -    )
    -
    -    document = json.loads(capsys.readouterr().err)
    -    assert "arbitrary_payload" not in document
    -
    -
    -def test_structured_logging_rejects_unowned_fields() -> None:
    -    with pytest.raises(ValueError, match="未知结构化日志字段"):
    -        log_event(
    -            logging.getLogger("test.observability"),
    -            logging.INFO,
    -            "test.invalid",
    -            arbitrary_payload="must not be logged",
    -        )
    -
    -
    -def test_json_formatter_promotes_existing_diagnostic_events(
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_LOG_FORMAT", "json")
    -    configure_logging()
    -
    -    logging.getLogger("test.legacy").info(
    -        diagnostic_line("PassiveTurnPipeline.run", event="phase_error")
    -    )
    -
    -    document = json.loads(capsys.readouterr().err)
    -    assert document["event"] == "phase_error"
    -    assert document["operation"] == "PassiveTurnPipeline.run"
    -
    -
    -def test_turn_log_id_prefers_persisted_control_turn() -> None:
    -    message = InboundMessage(
    -        channel="cli",
    -        sender="owner",
    -        chat_id="chat",
    -        content="hello",
    -        metadata={"turnId": "turn-persisted"},
    -    )
    -
    -    assert _turn_log_id("cli:chat", message) == "turn-persisted"
    -
    -
    -def test_turn_log_id_marks_pre_persistence_fallback() -> None:
    -    message = InboundMessage(
    -        channel="cli",
    -        sender="owner",
    -        chat_id="chat",
    -        content="hello",
    -    )
    -
    -    assert _turn_log_id("cli:chat", message).startswith("local-")
    -
    -
    -def test_turn_milestone_text_message_contains_full_identity(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    with caplog.at_level(logging.INFO, logger="test.observability"):
    -        turn_milestone(
    -            logging.getLogger("test.observability"),
    -            "tl:send.received",
    -            session_id="session-1",
    -            turn_id="turn-1",
    -            client_message_id="client-1",
    -            duration_ms=12.3,
    -            counts="n=1",
    -            outcome="accepted",
    -        )
    -
    -    message = caplog.records[0].getMessage()
    -    assert "event=tl:send.received" in message
    -    assert "session_id=session-1" in message
    -    assert "turn_id=turn-1" in message
    -    assert "client_message_id=client-1" in message
    -    assert "duration_ms=12.3" in message
    -    assert "origin=monotonic" in message
    -    assert "outcome=accepted" in message
    -    assert "counts=n=1" in message
    -    assert "request_id=" not in message
    -    assert "session=" not in message
    -    assert "turn=" not in message
    -
    -
    -def test_turn_milestone_marks_missing_fields_without_fabricating_duration(
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    with caplog.at_level(logging.INFO, logger="test.observability"):
    -        turn_milestone(
    -            logging.getLogger("test.observability"),
    -            "tl:turn.started",
    -            session_id="session-1",
    -        )
    -
    -    message = caplog.records[0].getMessage()
    -    assert "turn_id=missing" in message
    -    assert "client_message_id=missing" in message
    -    assert "duration_ms=missing" in message
    -    assert "origin=missing" in message
    -    assert "outcome=missing" in message
    -    assert "counts=missing" in message
    -    assert "duration_ms=0" not in message
    -
    -
    -def test_turn_milestone_json_extra_uses_same_field_names(
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_LOG_FORMAT", "json")
    -    configure_logging()
    -
    -    turn_milestone(
    -        logging.getLogger("test.observability"),
    -        "tl:send.ack",
    -        session_id="session-1",
    -        turn_id="turn-1",
    -        client_message_id="client-1",
    -        duration_ms=12.34,
    -        counts="n=1",
    -        outcome="accepted",
    -    )
    -
    -    document = json.loads(capsys.readouterr().err)
    -    assert document["event"] == "tl:send.ack"
    -    assert document["session_id"] == "session-1"
    -    assert document["turn_id"] == "turn-1"
    -    assert document["client_message_id"] == "client-1"
    -    assert document["duration_ms"] == 12.3
    -    assert document["origin"] == "monotonic"
    -    assert document["outcome"] == "accepted"
    -    assert document["counts"] == "n=1"
    -    assert "request_id" not in document
    -    assert "session" not in document
    -    assert "turn" not in document
    -    assert "duration_ms=12.3" in document["message"]
    -    assert "client_message_id=client-1" in document["message"]
    -
    -
    -def test_reply_sent_json_preserves_epoch_integer_and_replay_boolean(
    -    monkeypatch: pytest.MonkeyPatch,
    -    capsys: pytest.CaptureFixture[str],
    -) -> None:
    -    monkeypatch.setenv("AKASHIC_LOG_FORMAT", "json")
    -    configure_logging()
    -
    -    for replayed in (False, True):
    -        turn_milestone(
    -            logging.getLogger("test.observability"),
    -            "tl:send.reply_sent",
    -            session_id="mobile:session-1",
    -            client_message_id="client-1",
    -            duration_ms=1.25,
    -            outcome="receipt_replayed" if replayed else "sent",
    -            device_id="device-1",
    -            connection_epoch=7,
    -            reply_type="message.send.ok",
    -            receipt_replayed=replayed,
    -        )
    -
    -    documents = [
    -        json.loads(line)
    -        for line in capsys.readouterr().err.splitlines()
    -        if line.strip()
    -    ]
    -    assert len(documents) == 2
    -    assert [document["connection_epoch"] for document in documents] == [7, 7]
    -    assert [document["receipt_replayed"] for document in documents] == [
    -        False,
    -        True,
    -    ]
    -    assert all(isinstance(document["connection_epoch"], int) for document in documents)
    -    assert all(isinstance(document["receipt_replayed"], bool) for document in documents)
    -
    -
    -@pytest.mark.asyncio
    -async def test_control_terminal_milestone_uses_inbound_metadata_client_message_id(
    -    tmp_path: Path,
    -    caplog: pytest.LogCaptureFixture,
    -) -> None:
    -    bus = EventBus()
    -
    -    class _Loop:
    -        async def process_direct_message(
    -            self,
    -            _content: str,
    -            **kwargs: object,
    -        ) -> OutboundMessage:
    -            turn_id = str(kwargs["turn_id"])
    -            await bus.fanout(
    -                TurnCommitted(
    -                    session_key="mobile:one",
    -                    channel="mobile",
    -                    chat_id="one",
    -                    input_message="hello",
    -                    persisted_user_message="hello",
    -                    assistant_response="done",
    -                    tools_used=[],
    -                    turn_id=turn_id,
    -                )
    -            )
    -            return OutboundMessage("mobile", "one", "done")
    -
    -    store = SessionStore(tmp_path / "sessions.db")
    -
    -    async def execute(request: TurnRequest):
    -        from bootstrap.control_execution import execute_control_turn
    -
    -        return await execute_control_turn(cast(Any, _Loop()), bus, request)
    -
    -    runtime = ConversationRuntime(store, execute)
    -    with caplog.at_level(logging.INFO, logger="agent.control.runtime"):
    -        result = await (
    -            await runtime.start_turn(
    -                TurnRequest(
    -                    "mobile:one",
    -                    "hello",
    -                    {
    -                        "channel": "mobile",
    -                        "chatId": "one",
    -                        "runtime": "latest",
    -                        "inboundMetadata": {"client_message_id": "client-1"},
    -                    },
    -                )
    -            )
    -        ).result()
    -    assert result.status.value == "completed"
    -    terminal = next(
    -        record
    -        for record in caplog.records
    -        if record.akashic_fields.get("event") == "tl:turn.terminal"
    -    )
    -    assert terminal.akashic_fields["session_id"] == "mobile:one"
    -    assert terminal.akashic_fields["client_message_id"] == "client-1"
    -    assert terminal.akashic_fields["outcome"] == "completed"
    -    assert "session_id=mobile:one" in terminal.getMessage()
    -    assert "client_message_id=client-1" in terminal.getMessage()
    -    assert "outcome=completed" in terminal.getMessage()
    -    await runtime.shutdown()
    -    await bus.aclose()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_control_terminal_fails_loud_on_malformed_client_message_id(
    -    tmp_path: Path,
    -) -> None:
    -    bus = EventBus()
    -    store = SessionStore(tmp_path / "sessions.db")
    -
    -    async def execute(request: TurnRequest):
    -        raise AssertionError("结构不符的 turn 不应执行")
    -
    -    runtime = ConversationRuntime(store, execute)
    -    with pytest.raises(ValueError, match="client_message_id"):
    -        await runtime.start_turn(
    -            TurnRequest(
    -                "mobile:one",
    -                "hello",
    -                {
    -                    "channel": "mobile",
    -                    "chatId": "one",
    -                    "inboundMetadata": {"client_message_id": 123},
    -                },
    -            )
    -        )
    -    await runtime.shutdown()
    -    await bus.aclose()
    -    store.close()
    diff --git a/tests/test_subagent_v3_runtime.py b/tests/test_subagent_v3_runtime.py
    deleted file mode 100644
    index 1d42a2965..000000000
    --- a/tests/test_subagent_v3_runtime.py
    +++ /dev/null
    @@ -1,269 +0,0 @@
    -from __future__ import annotations
    -
    -import asyncio
    -import re
    -from pathlib import Path
    -from typing import Any
    -
    -import pytest
    -
    -from agent.control.models import TurnRequest
    -from agent.control.ports import ControlExecutionResult
    -from agent.control.runtime import ConversationRuntime
    -from agent.control.turn_scope import get_current_turn_scope
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from bus.events import InboundMessage
    -from session.store import SessionStore
    -
    -
    -async def _wait_until(predicate: Any, *, attempts: int = 200) -> None:
    -    for _ in range(attempts):
    -        if predicate():
    -            return
    -        await asyncio.sleep(0.01)
    -    raise AssertionError("condition did not settle")
    -
    -
    -async def _loaded_runtime(
    -    tmp_path: Path,
    -    execute: Any,
    -) -> tuple[SessionStore, ConversationRuntime, PluginManager, list[InboundMessage]]:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    store = SessionStore(workspace / "sessions.db")
    -    conversation = ConversationRuntime(store, execute)
    -    delivered: list[InboundMessage] = []
    -
    -    async def publish(item: InboundMessage) -> None:
    -        delivered.append(item)
    -
    -    manager = PluginManager(
    -        plugin_dirs=[Path(__file__).resolve().parents[1] / "plugins" / "subagent"],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        conversation,
    -        programmatic_session_creator=store.create_session,
    -    )
    -    manager.bind_continuation_publisher(publish)
    -    await manager.load_all()
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert snapshot.tool_registry is not None
    -    assert {"spawn", "spawn_manage"}.issubset(
    -        snapshot.tool_registry.get_registered_names()
    -    )
    -    return store, conversation, manager, delivered
    -
    -
    -async def _execute_tool(
    -    manager: PluginManager,
    -    name: str,
    -    arguments: dict[str, object],
    -    *,
    -    turn_id: str,
    -    origin_channel: str = "",
    -    origin_chat_id: str = "",
    -) -> str:
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None and snapshot.tool_registry is not None
    -    lease = manager.snapshot_store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    snapshot.tool_registry.set_context(
    -        turn_id=turn_id,
    -        origin_channel=origin_channel,
    -        origin_chat_id=origin_chat_id,
    -    )
    -    try:
    -        result = await snapshot.tool_registry.execute(
    -            name, arguments, raise_errors=True
    -        )
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -    assert isinstance(result, str)
    -    return result
    -
    -
    -@pytest.mark.asyncio
    -async def test_subagent_profiles_freeze_exact_tools_and_task_roots(
    -    tmp_path: Path,
    -) -> None:
    -    observed: list[object] = []
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        scope = get_current_turn_scope()
    -        assert scope is not None
    -        observed.append(scope)
    -        return ControlExecutionResult(response="done")
    -
    -    store, conversation, manager, _ = await _loaded_runtime(tmp_path, execute)
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    for profile in ("research", "scripting", "general"):
    -        result = await _execute_tool(
    -            manager,
    -            "spawn",
    -            {"task": f"inspect {profile}", "profile": profile},
    -            turn_id=f"parent:{profile}",
    -        )
    -        assert "done" in result
    -
    -    research, scripting, general = observed
    -    assert research.tool_overrides == {}
    -    assert set(scripting.tool_overrides) == {
    -        "write_file",
    -        "edit_file",
    -        "shell",
    -        "write_stdin",
    -        "task_stop",
    -    }
    -    assert scripting.tool_overrides["shell"]._allow_network is False
    -    assert general.tool_overrides["shell"]._allow_network is True
    -    scripting_root = scripting.tool_overrides["write_file"]._allowed_dir
    -    general_root = general.tool_overrides["write_file"]._allowed_dir
    -    assert scripting_root.parent == tmp_path / "workspace" / "subagent-runs"
    -    assert general_root.parent == tmp_path / "workspace" / "subagent-runs"
    -    assert scripting_root != general_root
    -    assert snapshot.lease_count == 0
    -    await manager.terminate_all()
    -    await conversation.shutdown()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_background_completion_is_exactly_once_and_releases_lease(
    -    tmp_path: Path,
    -) -> None:
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        return ControlExecutionResult(response="fixture-result")
    -
    -    store, conversation, manager, delivered = await _loaded_runtime(tmp_path, execute)
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    receipt = await _execute_tool(
    -        manager,
    -        "spawn",
    -        {
    -            "task": "complete a bounded four-step fixture investigation",
    -            "label": "fixture",
    -            "profile": "research",
    -            "run_in_background": True,
    -        },
    -        turn_id="parent:success",
    -        origin_channel="web",
    -        origin_chat_id="chat-1",
    -    )
    -    assert "job_id=" in receipt
    -    await _wait_until(lambda: len(delivered) == 1 and snapshot.lease_count == 0)
    -    assert delivered[0].channel == "web"
    -    assert delivered[0].chat_id == "chat-1"
    -    assert "fixture-result" in delivered[0].content
    -    await asyncio.sleep(0.05)
    -    assert len(delivered) == 1
    -    assert snapshot.lease_count == 0
    -    await manager.terminate_all()
    -    await conversation.shutdown()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_cancel_announces_before_interrupt_and_never_late_succeeds(
    -    tmp_path: Path,
    -) -> None:
    -    started = asyncio.Event()
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        started.set()
    -        await asyncio.Future()
    -        raise AssertionError("unreachable")
    -
    -    store, conversation, manager, delivered = await _loaded_runtime(tmp_path, execute)
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    receipt = await _execute_tool(
    -        manager,
    -        "spawn",
    -        {
    -            "task": "run a bounded fixture until cancellation is requested",
    -            "label": "cancel",
    -            "run_in_background": True,
    -        },
    -        turn_id="parent:cancel",
    -        origin_channel="web",
    -        origin_chat_id="chat-2",
    -    )
    -    await started.wait()
    -    match = re.search(r"job_id=([0-9a-f]+)", receipt)
    -    assert match is not None
    -    result = await _execute_tool(
    -        manager,
    -        "spawn_manage",
    -        {"action": "cancel", "job_id": match.group(1)},
    -        turn_id="parent:cancel",
    -    )
    -    assert "cancel_requested" in result
    -    assert len(delivered) == 1
    -    assert "已取消" in delivered[0].content
    -    await _wait_until(lambda: snapshot.lease_count == 0)
    -    await asyncio.sleep(0.05)
    -    assert len(delivered) == 1
    -    assert "fixture-result" not in delivered[0].content
    -    assert snapshot.lease_count == 0
    -    await manager.terminate_all()
    -    await conversation.shutdown()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_capacity_rejects_fourth_child_without_creating_turn(
    -    tmp_path: Path,
    -) -> None:
    -    blocker = asyncio.Event()
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        await blocker.wait()
    -        return ControlExecutionResult(response="released")
    -
    -    store, conversation, manager, _ = await _loaded_runtime(tmp_path, execute)
    -    receipts: list[str] = []
    -    try:
    -        for index in range(3):
    -            receipts.append(
    -                await _execute_tool(
    -                    manager,
    -                    "spawn",
    -                    {
    -                        "task": f"complete bounded fixture investigation number {index}",
    -                        "run_in_background": True,
    -                    },
    -                    turn_id=f"parent:{index}",
    -                    origin_channel="web",
    -                    origin_chat_id="chat-capacity",
    -                )
    -            )
    -        rejected = await _execute_tool(
    -            manager,
    -            "spawn",
    -            {
    -                "task": "complete bounded fixture investigation number fourth",
    -                "run_in_background": True,
    -            },
    -            turn_id="parent:fourth",
    -            origin_channel="web",
    -            origin_chat_id="chat-capacity",
    -        )
    -        assert "上限 3" in rejected
    -        assert len(store.list_sessions()) == 3
    -    finally:
    -        blocker.set()
    -    await _wait_until(lambda: manager.current_snapshot.lease_count == 0)
    -    await manager.terminate_all()
    -    await conversation.shutdown()
    -    store.close()
    diff --git a/tests/test_subagent_v3_shadow.py b/tests/test_subagent_v3_shadow.py
    deleted file mode 100644
    index b94aad6ad..000000000
    --- a/tests/test_subagent_v3_shadow.py
    +++ /dev/null
    @@ -1,135 +0,0 @@
    -from __future__ import annotations
    -
    -from pathlib import Path
    -from typing import cast
    -
    -import pytest
    -
    -from agent.control.models import TurnRequest
    -from agent.control.ports import ControlExecutionResult
    -from agent.control.runtime import ConversationRuntime
    -from agent.control.turn_scope import get_current_turn_scope
    -from agent.turn_effects import PostCommitEffect, TurnStorage
    -from agent.plugin_composition import SCOPED_TURNS
    -from agent.plugins.manager import PluginManager
    -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot
    -from agent.tools.registry import ToolRegistry
    -from bus.event_bus import EventBus
    -from session.store import SessionStore
    -
    -
    -@pytest.mark.asyncio
    -async def test_builtin_subagent_shadow_recurses_through_scoped_turn_service(
    -    tmp_path: Path,
    -) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    store = SessionStore(workspace / "sessions.db")
    -    observed: dict[str, object] = {}
    -
    -    async def execute(request: TurnRequest) -> ControlExecutionResult:
    -        scope = get_current_turn_scope()
    -        assert scope is not None
    -        observed.update(
    -            {
    -                "thread_id": request.thread_id,
    -                "input": request.input,
    -                "prompt_hints": scope.prompt_hints,
    -                "grant": scope.tool_grant.names,
    -                "disabled_prompt_sections": scope.disabled_prompt_sections,
    -                "storage": scope.storage,
    -                "post_commit_effect": scope.post_commit_effect,
    -                "tool_source": scope.tool_source,
    -            }
    -        )
    -        return ControlExecutionResult(response="child:done")
    -
    -    runtime = ConversationRuntime(store, execute)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(__file__).resolve().parents[1] / "plugins" / "subagent"],
    -        event_bus=EventBus(),
    -        tool_registry=ToolRegistry(validate_semantic_schema=False),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        runtime,
    -        programmatic_session_creator=store.create_session,
    -    )
    -    manager.bind_continuation_publisher(lambda _item: _noop())
    -    await manager.load_all()
    -    snapshot = manager.current_snapshot
    -    assert snapshot is not None
    -    assert snapshot.tool_registry is not None
    -
    -    lease = manager.snapshot_store.lease()
    -    token = bind_runtime_snapshot(lease)
    -    snapshot.tool_registry.set_context(turn_id="parent:turn")
    -    try:
    -        result = await snapshot.tool_registry.execute(
    -            "spawn",
    -            {"task": "inspect the fixture", "profile": "research"},
    -            raise_errors=True,
    -        )
    -    finally:
    -        reset_runtime_snapshot(token)
    -        await lease.release()
    -
    -    assert isinstance(result, str)
    -    assert "child:done" in result
    -    assert observed["input"] == "inspect the fixture"
    -    assert observed["disabled_prompt_sections"] == frozenset({"memory"})
    -    assert observed["storage"] is TurnStorage.IN_MEMORY
    -    assert observed["post_commit_effect"] is PostCommitEffect.SUPPRESS
    -    assert observed["tool_source"] == "subagent"
    -    assert observed["grant"] == frozenset(
    -        {"read_file", "list_dir", "web_fetch", "web_search"}
    -    )
    -    assert "调研型子 agent" in cast(tuple[str, ...], observed["prompt_hints"])[0]
    -    assert len(list((workspace / "subagent-runs").iterdir())) == 1
    -    trace = workspace / "memory" / "spawn_trace.jsonl"
    -    assert trace.read_text(encoding="utf-8").count("\n") == 2
    -
    -    await runtime.shutdown()
    -    store.close()
    -
    -
    -@pytest.mark.asyncio
    -async def test_subagent_candidate_service_denies_child_turns(tmp_path: Path) -> None:
    -    workspace = tmp_path / "workspace"
    -    workspace.mkdir()
    -    store = SessionStore(workspace / "sessions.db")
    -
    -    async def execute(_request: TurnRequest) -> ControlExecutionResult:
    -        raise AssertionError("candidate must not create a child Turn")
    -
    -    runtime = ConversationRuntime(store, execute)
    -    manager = PluginManager(
    -        plugin_dirs=[Path(__file__).resolve().parents[1] / "plugins" / "subagent"],
    -        event_bus=EventBus(),
    -        workspace=workspace,
    -        installed_cache_root=tmp_path / "cache",
    -    )
    -    manager.bind_conversation_runtime(
    -        runtime,
    -        programmatic_session_creator=store.create_session,
    -    )
    -    manager.bind_continuation_publisher(lambda _item: _noop())
    -    await manager.load_all()
    -    candidate = await manager.prepare_candidate("subagent")
    -    assert candidate is not None
    -    assert candidate.runtime_snapshot is not None
    -    root = candidate.runtime_snapshot.composition_root
    -    assert root is not None
    -    service = root.context.require(SCOPED_TURNS)
    -    with pytest.raises(RuntimeError, match="candidate 验证期"):
    -        await service.create_session(metadata={"source": "subagent"})
    -
    -    assert store.list_sessions() == []
    -    await manager.discard_prepared("subagent")
    -    await runtime.shutdown()
    -    store.close()
    -
    -
    -async def _noop() -> None:
    -    return None
    diff --git a/tests/test_support_modules.py b/tests/test_support_modules.py
    deleted file mode 100644
    index d1638725e..000000000
    --- a/tests/test_support_modules.py
    +++ /dev/null
    @@ -1,844 +0,0 @@
    -from __future__ import annotations
    -import asyncio
    -import json
    -import runpy
    -import sys
    -from datetime import datetime, timedelta, timezone
    -from pathlib import Path
    -from types import SimpleNamespace
    -from unittest.mock import AsyncMock, MagicMock
    -
    -import pytest
    -
    -from agent.context import ContextBuilder, ContextRequest
    -from agent.persona import reset_veda
    -from agent.prompting import PromptSectionRender, SYSTEM_CONTEXT_FRAME_MARKER
    -from agent.tools.base import Tool
    -from agent.tools.message_push import MessagePushTool
    -from agent.plugin_composition.channels import (
    -    ChannelDeliveryReceipt,
    -    DeliveryStatus as ChannelDeliveryStatus,
    -)
    -from agent.tools.registry import ToolMeta, ToolRegistry
    -from agent.tools.web_search import WebSearchTool
    -from bus.events import (
    -    AttachmentKind,
    -    ChannelAttachment,
    -    ChannelMessage,
    -    InboundMessage,
    -    OutboundMessage,
    -    TurnTerminalStatus,
    -)
    -from bus.queue import ChatLane, MessageBus
    -from core.common import timekit
    -from infra.persistence.json_store import atomic_save_json, load_json, save_json
    -from prompts.agent import build_agent_behavior_rules_prompt
    -from prompts.completion import VERIFIABLE_COMPLETION_RULES
    -
    -
    -class _MemoryProfileStub:
    -    def read_long_term(self) -> str:
    -        return ""
    -
    -    def write_long_term(self, content: str) -> None:
    -        pass
    -
    -    def read_self(self) -> str:
    -        return ""
    -
    -    def write_self(self, content: str) -> None:
    -        pass
    -
    -    def backup_long_term(self, backup_name: str = "MEMORY.bak.md") -> None:
    -        pass
    -
    -    def backup_self(self, backup_name: str = "SELF.bak.md") -> None:
    -        pass
    -
    -    def get_memory_context(self) -> str:
    -        return ""
    -
    -    def has_long_term_memory(self) -> bool:
    -        return False
    -
    -
    -def test_inbound_message_default_timestamp_is_aware_utc() -> None:
    -    message = InboundMessage(
    -        channel="test",
    -        sender="user",
    -        chat_id="one",
    -        content="hello",
    -    )
    -
    -    assert message.timestamp.tzinfo is timezone.utc
    -
    -
    -def test_agent_prompt_uses_authoritative_completion_rules(tmp_path: Path) -> None:
    -    prompt = build_agent_behavior_rules_prompt(workspace=tmp_path)
    -
    -    assert VERIFIABLE_COMPLETION_RULES in prompt
    -    assert "每个主要工具结果后都要把新增证据对应到用户明确提出的要求" in prompt
    -    assert 'transport_status="success"' in prompt
    -    assert "只补尚未证明要求的最小缺口" in prompt
    -
    -
    -class _DummyTool(Tool):
    -    @property
    -    def name(self) -> str:
    -        return "dummy"
    -
    -    @property
    -    def description(self) -> str:
    -        return "dummy description"
    -
    -    @property
    -    def parameters(self) -> dict:
    -        return {
    -            "type": "object",
    -            "properties": {
    -                "name": {"type": "string", "minLength": 2},
    -                "count": {"type": "integer", "minimum": 1, "maximum": 3},
    -                "mode": {"type": "string", "enum": ["a", "b"]},
    -                "items": {"type": "array", "items": {"type": "number"}},
    -            },
    -            "required": ["name", "count"],
    -        }
    -
    -    async def execute(self, **kwargs) -> str:
    -        return json.dumps(kwargs, ensure_ascii=False)
    -
    -
    -@pytest.mark.asyncio
    -async def test_message_push_dispatches_exact_v3_receipt_and_media():
    -    tool = MessagePushTool()
    -    seen: list[tuple[ChannelMessage, bool]] = []
    -
    -    async def dispatch(
    -        message: ChannelMessage, passive: bool
    -    ) -> ChannelDeliveryReceipt:
    -        seen.append((message, passive))
    -        return ChannelDeliveryReceipt(
    -            delivery_id="delivery-1",
    -            status=ChannelDeliveryStatus.DELIVERED,
    -            provider_ids=("provider-1",),
    -        )
    -
    -    tool.bind_v3_channel_dispatcher(dispatch)
    -    result = json.loads(
    -        await tool.execute(
    -            target_channel="telegram",
    -            target_chat_id=123,
    -            message="hello",
    -            file="/tmp/demo.txt",
    -            image="https://img",
    -        )
    -    )
    -
    -    assert result == {
    -        "delivery_id": "delivery-1",
    -        "status": "delivered",
    -        "retryable": False,
    -        "provider_ids": ["provider-1"],
    -        "error": None,
    -    }
    -    assert not hasattr(tool, "register_channel")
    -    assert seen[0][1] is False
    -    assert seen[0][0].attachments == (
    -        ChannelAttachment(AttachmentKind.FILE, "/tmp/demo.txt", "demo.txt"),
    -        ChannelAttachment(AttachmentKind.IMAGE, "https://img"),
    -    )
    -    assert seen[0][0].metadata == {"source": "message_push"}
    -
    -
    -@pytest.mark.asyncio
    -async def test_message_push_missing_committed_dispatcher_fails_loud() -> None:
    -    tool = MessagePushTool()
    -
    -    with pytest.raises(RuntimeError, match="committed Channel dispatcher 未绑定"):
    -        await tool.execute(
    -            target_channel="telegram",
    -            target_chat_id="1",
    -            message="hello",
    -        )
    -
    -
    -@pytest.mark.asyncio
    -async def test_message_push_passive_role_is_forwarded_to_committed_dispatcher() -> None:
    -    tool = MessagePushTool()
    -    passive_roles: list[bool] = []
    -    messages: list[ChannelMessage] = []
    -
    -    async def dispatch(
    -        _message: ChannelMessage,
    -        passive: bool,
    -    ) -> ChannelDeliveryReceipt:
    -        passive_roles.append(passive)
    -        messages.append(_message)
    -        return ChannelDeliveryReceipt(
    -            delivery_id="delivery-passive",
    -            status=ChannelDeliveryStatus.UNKNOWN,
    -            error="provider outcome unknown",
    -        )
    -
    -    tool.bind_v3_channel_dispatcher(dispatch)
    -    result = json.loads(
    -        await tool.execute(
    -            target_channel="mobile",
    -            target_chat_id="1",
    -            message="final",
    -            _commit_role="passive",
    -        )
    -    )
    -
    -    assert passive_roles == [True]
    -    assert messages[0].metadata == {"source": "message_push"}
    -    assert result["status"] == "unknown"
    -    assert result["retryable"] is False
    -
    -
    -@pytest.mark.asyncio
    -async def test_passive_terminal_dispatch_does_not_become_message_push() -> None:
    -    tool = MessagePushTool()
    -    messages: list[ChannelMessage] = []
    -
    -    async def dispatch(
    -        message: ChannelMessage,
    -        _passive: bool,
    -    ) -> ChannelDeliveryReceipt:
    -        messages.append(message)
    -        return ChannelDeliveryReceipt(
    -            delivery_id="delivery-terminal",
    -            status=ChannelDeliveryStatus.DELIVERED,
    -        )
    -
    -    tool.bind_v3_channel_dispatcher(dispatch)
    -    await tool.dispatch(ChannelMessage(
    -        channel="akashic",
    -        chat_id="session",
    -        content="普通最终回复",
    -        control_turn_id="turn:normal",
    -        terminal_status=TurnTerminalStatus.COMPLETED,
    -    ), commit_role="passive")
    -
    -    assert messages[0].metadata == {}
    -    assert messages[0].terminal_status is TurnTerminalStatus.COMPLETED
    -
    -
    -@pytest.mark.asyncio
    -async def test_chat_lane_cancelled_non_passive_waiter_does_not_wedge_lane():
    -    lane = ChatLane()
    -    ran: list[str] = []
    -
    -    async def first_send() -> None:
    -        ran.append("first")
    -
    -    async def second_send() -> None:
    -        ran.append("second")
    -
    -    await lane.mark_passive_pending("cli", "1")
    -    with pytest.raises(asyncio.TimeoutError):
    -        await asyncio.wait_for(
    -            lane.run_non_passive("cli", "1", first_send),
    -            timeout=0.01,
    -        )
    -
    -    await lane.mark_passive_done("cli", "1")
    -    await asyncio.wait_for(
    -        lane.run_non_passive("cli", "1", second_send),
    -        timeout=1,
    -    )
    -
    -    assert ran == ["second"]
    -    assert lane._states == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_chat_lane_releases_idle_state_after_send_error():
    -    lane = ChatLane()
    -
    -    await lane.mark_passive_pending("cli", "1")
    -    await lane.mark_passive_done("cli", "1")
    -    assert lane._states == {}
    -
    -    async def failed_send() -> None:
    -        raise RuntimeError("send failed")
    -
    -    with pytest.raises(RuntimeError, match="send failed"):
    -        await lane.run_passive("cli", "1", failed_send)
    -
    -    assert lane._states == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_message_push_passive_send_does_not_consume_queued_outbound_pending():
    -    lane = ChatLane()
    -    events: list[str] = []
    -
    -    async def record(value: str) -> None:
    -        events.append(value)
    -
    -    await lane.mark_passive_send_pending("cli", "1")
    -    await lane.run_passive("cli", "1", lambda: record("push"))
    -    active = asyncio.create_task(
    -        lane.run_non_passive("cli", "1", lambda: record("active"))
    -    )
    -
    -    await asyncio.sleep(0.01)
    -    assert events == ["push"]
    -    assert not active.done()
    -
    -    await lane.run_passive(
    -        "cli",
    -        "1",
    -        lambda: record("outbound"),
    -        pending_registered=True,
    -    )
    -    await asyncio.wait_for(active, timeout=1)
    -
    -    assert events == ["push", "outbound", "active"]
    -    assert lane._states == {}
    -
    -
    -@pytest.mark.asyncio
    -async def test_web_search_covers_filters(monkeypatch: pytest.MonkeyPatch):
    -    class _Response:
    -        def __init__(self, text: str) -> None:
    -            self.text = text
    -
    -        def raise_for_status(self) -> None:
    -            return None
    -
    -    class _Client:
    -        def __init__(self, timeout: float) -> None:
    -            self.timeout = timeout
    -
    -        async def __aenter__(self):
    -            return self
    -
    -        async def __aexit__(self, exc_type, exc, tb):
    -            return False
    -
    -        async def post(self, url: str, json: dict, headers: dict) -> _Response:
    -            assert json["params"]["arguments"]["numResults"] == 20
    -            assert json["params"]["arguments"]["livecrawl"] == "preferred"
    -            assert json["params"]["arguments"]["type"] == "deep"
    -            return _Response(
    -                'data: {"result":{"content":[{"text":"hello world"}]}}\n\n'
    -            )
    -
    -    monkeypatch.setattr("httpx.AsyncClient", _Client)
    -    result = json.loads(
    -        await WebSearchTool().execute(
    -            query="搜索 网络",
    -            num_results=99,
    -            livecrawl="preferred",
    -            type="deep",
    -        )
    -    )
    -    assert result["result"] == "hello world"
    -
    -    class _BadClient(_Client):
    -        async def post(self, url: str, json: dict, headers: dict) -> _Response:
    -            raise RuntimeError("net down")
    -
    -    monkeypatch.setattr("httpx.AsyncClient", _BadClient)
    -    result = json.loads(await WebSearchTool().execute(query="x"))
    -    assert "搜索失败" in result["error"]
    -
    -    class _EmptyClient(_Client):
    -        async def post(self, url: str, json: dict, headers: dict) -> _Response:
    -            return _Response("data: not-json\n\ndata: {}")
    -
    -    monkeypatch.setattr("httpx.AsyncClient", _EmptyClient)
    -    result = json.loads(await WebSearchTool().execute(query="x"))
    -    assert result["count"] == 0
    -
    -
    -def test_tool_base_and_timekit_and_json_store_cover_branches(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    tool = _DummyTool()
    -    errors = tool.validate_params(
    -        {"name": "x", "count": 5, "mode": "c", "items": ["a"]}
    -    )
    -    assert "name 最短 2 个字符" in errors
    -    assert "count 须 <= 3" in errors
    -    assert "mode 须为以下值之一" in errors[2]
    -    assert "[0] 应为 number 类型" in errors[3]
    -    assert tool.validate_params({})[:2] == ["缺少必填字段:name", "缺少必填字段:count"]
    -    assert tool.to_schema()["function"]["name"] == "dummy"
    -
    -    numeric_type_errors = tool.validate_params(
    -        {"name": "ok", "count": True, "items": [False]}
    -    )
    -    assert numeric_type_errors == [
    -        "count 应为 integer 类型",
    -        "items[0] 应为 number 类型",
    -    ]
    -
    -    class _BadSchemaTool(_DummyTool):
    -        @property
    -        def parameters(self) -> dict:
    -            return {"type": "array"}
    -
    -    with pytest.raises(ValueError):
    -        _BadSchemaTool().validate_params({})
    -
    -    with pytest.raises(TypeError, match="必须定义字段:description, parameters"):
    -
    -        class _MissingTool(Tool):
    -            name = "bad"
    -
    -            async def execute(self, **kwargs) -> str:
    -                return "ok"
    -
    -    with pytest.raises(TypeError, match="字段不能为空:name, description, parameters"):
    -
    -        class _EmptyTool(Tool):
    -            name = ""
    -            description = ""
    -            parameters = {}
    -
    -            async def execute(self, **kwargs) -> str:
    -                return "ok"
    -
    -    path = tmp_path / "data.json"
    -    assert load_json(path, default={"a": 1}) == {"a": 1}
    -    save_json(path, {"x": "中"})
    -    assert load_json(path)["x"] == "中"
    -    path.write_text("{bad", encoding="utf-8")
    -    with pytest.raises(RuntimeError, match=r"\[json_store\].*data\.json"):
    -        load_json(path, default=[])
    -    atomic_save_json(path, {"y": 2})
    -    assert load_json(path)["y"] == 2
    -
    -    monkeypatch.setattr(
    -        "pathlib.Path.write_text",
    -        lambda self, *args, **kwargs: (_ for _ in ()).throw(RuntimeError("bad")),
    -    )
    -    with pytest.raises(RuntimeError):
    -        save_json(tmp_path / "x.json", {"x": 1})
    -
    -    monkeypatch.setattr(
    -        "infra.persistence.json_store.os.fsync",
    -        lambda _fd: (_ for _ in ()).throw(RuntimeError("bad")),
    -    )
    -    with pytest.raises(RuntimeError):
    -        atomic_save_json(tmp_path / "x.json", {"x": 1})
    -
    -    parsed = timekit.parse_iso("2025-06-01T09:00:00Z")
    -    assert parsed and parsed.tzinfo is not None
    -    assert timekit.parse_iso("bad") is None
    -    assert timekit.format_iso(datetime(2025, 1, 1)).endswith("+00:00")
    -    logger = MagicMock()
    -    assert str(timekit.safe_zone("bad/zone", logger=logger)) == "UTC"
    -    logger.warning.assert_called_once()
    -    assert timekit.local_now("UTC").tzinfo is not None
    -    assert timekit.utcnow().tzinfo is not None
    -
    -
    -@pytest.mark.asyncio
    -async def test_context_builder_debug_projection_is_turn_local(tmp_path: Path) -> None:
    -    """并发 render 只暴露调用 task 自己的诊断投影。"""
    -
    -    class _Memory(_MemoryProfileStub):
    -        pass
    -
    -    _ = reset_veda(tmp_path)
    -    builder = ContextBuilder(tmp_path)
    -    first_rendered = asyncio.Event()
    -    second_rendered = asyncio.Event()
    -
    -    async def render(marker: str) -> tuple[list[object], list[object], dict[str, str]]:
    -        # 1. 让两个 task 写入不同的 debug 与 turn injection 投影。
    -        result = builder.render(
    -            ContextRequest(
    -                history=[],
    -                current_message=marker,
    -                multimodal=True,
    -                turn_injection_prompt=marker,
    -            ),
    -            system_sections_top=[
    -                PromptSectionRender(
    -                    name=f"marker-{marker}",
    -                    content=marker,
    -                    is_static=False,
    -                )
    -            ],
    -        )
    -        if marker == "first":
    -            first_rendered.set()
    -            await second_rendered.wait()
    -        else:
    -            await first_rendered.wait()
    -            second_rendered.set()
    -
    -        # 2. 在另一 task 已完成 render 后读取,必须仍得到本 task 的值。
    -        return (
    -            list(result.debug_breakdown),
    -            list(builder.last_debug_breakdown),
    -            builder.last_assembled_contexts["turn_injection_context"],
    -        )
    -
    -    first, second = await asyncio.gather(render("first"), render("second"))
    -
    -    assert first[1] == first[0]
    -    assert second[1] == second[0]
    -    assert first[2] == {"turn_injection": "first"}
    -    assert second[2] == {"turn_injection": "second"}
    -
    -
    -def test_context_builder_builds_prompt_messages_and_assistant_blocks(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    _ = reset_veda(tmp_path)
    -
    -    class _Skills:
    -        def __init__(self, workspace: Path, **_: object) -> None:
    -            self.workspace = workspace
    -
    -        def get_always_skills(self) -> list[str]:
    -            return ["always"]
    -
    -        def load_skills_for_context(self, names: list[str]) -> str:
    -            return ",".join(names)
    -
    -        def build_skills_summary(self) -> str:
    -            return "skill summary"
    -
    -    class _Memory(_MemoryProfileStub):
    -        def read_long_term(self) -> str:
    -            return "memory block"
    -
    -        def read_self(self) -> str:
    -            return "self note"
    -
    -        def get_memory_context(self) -> str:
    -            return "memory block"
    -
    -    monkeypatch.setattr("agent.context.SkillsLoader", _Skills)
    -    monkeypatch.setattr(
    -        "agent.context.build_agent_static_identity_prompt", lambda **_: "identity"
    -    )
    -    monkeypatch.setattr(
    -        "agent.context.build_telegram_rendering_prompt", lambda: "\ntelegram prompt"
    -    )
    -    monkeypatch.setattr(
    -        "agent.context.build_skills_catalog_prompt", lambda text: f"catalog:{text}"
    -    )
    -
    -    image = tmp_path / "a.png"
    -    from PIL import Image
    -
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(image)
    -    document = tmp_path / "view.pdf"
    -    document.write_bytes(b"%PDF-1.4\n")
    -    now = datetime.now(timezone.utc)
    -    (tmp_path / "memory").mkdir(exist_ok=True)
    -    (tmp_path / "memory" / "SELF.md").write_text("self note", encoding="utf-8")
    -
    -    builder = ContextBuilder(tmp_path)
    -    result = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="",
    -            multimodal=True,
    -            skill_names=["extra"],
    -            message_timestamp=now,
    -            turn_injection_prompt="retrieved",
    -        )
    -    )
    -    prompt = result.system_prompt
    -    context_frame = result.messages[-2]["content"]
    -    assert "identity" in prompt
    -    assert "## 行为规范" in prompt
    -    assert "最终回复前逐项核对用户明确提出的要求" in result.messages[0]["content"]
    -    assert "超过验收标准的完美继续调用工具" in result.messages[0]["content"]
    -    assert "retrieved" not in prompt
    -    assert context_frame.startswith(SYSTEM_CONTEXT_FRAME_MARKER)
    -    assert "retrieved" in context_frame
    -    assert "memory block" not in prompt
    -    assert "Akashic 自我认知" not in prompt
    -    assert "## 环境" in prompt
    -    assert "# Memes" not in prompt
    -    assert "" not in prompt
    -    assert "catalog:skill summary" in prompt
    -    assert [item.name for item in builder.last_debug_breakdown][:2] == [
    -        "veda",
    -        "identity",
    -    ]
    -
    -    result2 = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="",
    -            multimodal=True,
    -            skill_names=["extra"],
    -            message_timestamp=now,
    -            turn_injection_prompt="retrieved",
    -        )
    -    )
    -    assert result2.system_prompt
    -    identity_meta = next(
    -        item for item in builder.last_debug_breakdown if item.name == "identity"
    -    )
    -    assert identity_meta.cache_hit is True
    -
    -    messages = builder.render(
    -        ContextRequest(
    -            history=[{"role": "assistant", "content": "hi"}],
    -            current_message="hello",
    -            multimodal=True,
    -            media=["https://img", str(image), str(document), str(tmp_path / "bad.txt")],
    -            skill_names=["extra"],
    -            channel="telegram",
    -            chat_id="42",
    -        )
    -    ).messages
    -    assert messages[0]["role"] == "system"
    -    assert "## 环境" in messages[0]["content"]
    -    assert "## Current Session" in messages[0]["content"]
    -    assert messages[-1]["role"] == "user"
    -    assert len(messages[-1]["content"]) == 3
    -    stamped_message = messages[-1]["content"][-1]["text"]
    -    assert stamped_message.startswith("[当前消息时间:")
    -    assert "[附加媒体]" in stamped_message
    -    assert f"- 文件路径: {document}" in stamped_message
    -    assert f"- 不可用媒体路径: {tmp_path / 'bad.txt'}" in stamped_message
    -    assert "request_time=" in stamped_message
    -    assert "今天=" in stamped_message
    -    assert "昨天=" in stamped_message
    -    assert "明天=" in stamped_message
    -    assert "后天=" in stamped_message
    -    assert "weekday=" in stamped_message
    -    assert builder.last_assembled_contexts["turn_injection_context"] == {}
    -
    -    extensionless_image = tmp_path / "24"
    -    Image.new("RGB", (2, 2), (255, 0, 0)).save(
    -        extensionless_image,
    -        format="PNG",
    -    )
    -    extensionless_content = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="直接看图",
    -            multimodal=True,
    -            media=[str(extensionless_image)],
    -        )
    -    ).messages[-1]["content"]
    -    assert isinstance(extensionless_content, list)
    -    assert extensionless_content[0]["type"] == "image_url"
    -    assert extensionless_content[0]["image_url"]["url"].startswith(
    -        "data:image/png;base64,"
    -    )
    -
    -    turn_injection = builder.build_turn_injection_context(turn_injection_prompt="pref")
    -    render_result = builder.render(
    -        ContextRequest(
    -            history=[{"role": "assistant", "content": "hi"}],
    -            current_message="hello",
    -            multimodal=True,
    -            media=["https://img", str(image), str(document), str(tmp_path / "bad.txt")],
    -            skill_names=["extra"],
    -            channel="telegram",
    -            chat_id="42",
    -            message_timestamp=now,
    -            turn_injection_prompt="pref",
    -        )
    -    )
    -    assert render_result.system_prompt
    -    assert render_result.turn_injection_context == turn_injection
    -    assert render_result.messages
    -    assert render_result.messages[-2]["role"] == "user"
    -    assert render_result.messages[-2]["content"].startswith(SYSTEM_CONTEXT_FRAME_MARKER)
    -    assert "pref" in render_result.messages[-2]["content"]
    -
    -    custom_telegram = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="hello",
    -            multimodal=True,
    -            channel="telegram_work",
    -            chat_id="42",
    -            message_timestamp=now,
    -        )
    -    )
    -    assert "telegram prompt" in custom_telegram.messages[0]["content"]
    -
    -    media_only_messages = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="",
    -            multimodal=True,
    -            media=["https://img"],
    -            skill_names=["extra"],
    -            message_timestamp=now,
    -        )
    -    ).messages
    -    media_only_text = media_only_messages[-1]["content"][-1]["text"]
    -    assert media_only_text.startswith("[当前消息时间:")
    -    assert "request_time=" in media_only_text
    -    assert "今天=" in media_only_text
    -
    -    text_media_builder = ContextBuilder(tmp_path)
    -    text_media_messages = text_media_builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="看看这张图",
    -            multimodal=False,
    -            media=[str(image), str(document), str(tmp_path / "bad.txt")],
    -            skill_names=["extra"],
    -            message_timestamp=now,
    -        )
    -    ).messages
    -    text_media_content = text_media_messages[-1]["content"]
    -    assert isinstance(text_media_content, str)
    -    assert str(image) in text_media_content
    -    assert str(document) in text_media_content
    -    assert f"- 不可用媒体路径: {tmp_path / 'bad.txt'}" in text_media_content
    -    assert "read_image_vision" in text_media_content
    -    assert "image_url" not in text_media_content
    -
    -    missing_media_content = text_media_builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="附件呢",
    -            multimodal=False,
    -            media=[str(tmp_path / "bad.txt")],
    -        )
    -    ).messages[-1]["content"]
    -    assert f"- 不可用媒体路径: {tmp_path / 'bad.txt'}" in missing_media_content
    -    assert "没有可供 read_image_vision 读取的本地图片" in missing_media_content
    -    assert "read_image_vision(path=" not in missing_media_content
    -
    -
    -def test_multimodal_context_limits_image_count_and_encoded_total(
    -    monkeypatch: pytest.MonkeyPatch,
    -    tmp_path: Path,
    -) -> None:
    -    _ = reset_veda(tmp_path)
    -    builder = ContextBuilder(tmp_path)
    -    with pytest.raises(ValueError, match="最多可以添加 4 张图片"):
    -        builder.render(
    -            ContextRequest(
    -                history=[],
    -                current_message="too many",
    -                multimodal=True,
    -                media=[f"https://example.test/{index}.png" for index in range(5)],
    -            )
    -        )
    -
    -    first = tmp_path / "first.png"
    -    second = tmp_path / "second.png"
    -    first.write_bytes(b"fixture")
    -    second.write_bytes(b"fixture")
    -    monkeypatch.setattr("agent.context.MAX_IMAGE_DATA_URI_TOTAL_BYTES", 100)
    -    monkeypatch.setattr(
    -        "agent.context.encode_image_data_uri",
    -        lambda _path: "data:image/png;base64," + "A" * 60,
    -    )
    -    with pytest.raises(ValueError, match="合计大小超过"):
    -        builder.render(
    -            ContextRequest(
    -                history=[],
    -                current_message="too large",
    -                multimodal=True,
    -                media=[str(first), str(second)],
    -            )
    -        )
    -
    -
    -def test_context_builder_reproduces_temporal_conflict_baseline(
    -    monkeypatch: pytest.MonkeyPatch, tmp_path: Path
    -):
    -    _ = reset_veda(tmp_path)
    -
    -    class _Skills:
    -        def __init__(self, workspace: Path, **_: object) -> None:
    -            self.workspace = workspace
    -
    -        def get_always_skills(self) -> list[str]:
    -            return []
    -
    -        def load_skills_for_context(self, names: list[str]) -> str:
    -            return ""
    -
    -        def build_skills_summary(self) -> str:
    -            return ""
    -
    -    class _Memory(_MemoryProfileStub):
    -        pass
    -
    -    monkeypatch.setattr("agent.context.SkillsLoader", _Skills)
    -    monkeypatch.setattr(
    -        "agent.context.build_agent_static_identity_prompt", lambda **_: "identity"
    -    )
    -    monkeypatch.setattr("agent.context.build_telegram_rendering_prompt", lambda: "")
    -    monkeypatch.setattr("agent.context.build_skills_catalog_prompt", lambda text: text)
    -
    -    (tmp_path / "memes").mkdir()
    -    (tmp_path / "memes" / "manifest.json").write_text(
    -        '{"version":1,"categories":{}}',
    -        encoding="utf-8",
    -    )
    -
    -    builder = ContextBuilder(tmp_path)
    -    request_time = datetime.fromisoformat("2026-04-08T17:57:00+08:00")
    -    local_request_time = request_time.astimezone()
    -    turn_injection_prompt = """
    -[item_5a9c8d59f77c] [2026-03-29 12:44] 用户表示明天下午三点有面试,因当前感到疲惫想小睡,但担心此举会打乱明天的生物钟。
    -证据: 用户消息「明天我下午三点面试 我现在睡一会会打乱明天发生物钟吗有点疲惫」
    -
    -[item_87aa0364de9e] [2026-03-29 14:42] 用户因午睡未成功,转为练习力扣题目以准备次日下午三点的字节跳动面试。
    -证据: 用户消息「没睡着做会力扣准备明天面试了」
    -
    -[item_recent_interview] [2026-04-07 23:10] 用户提到 4 月 9 日(周四)下午 3 点的面试安排。
    -证据: 可回源原文「4 月 9 日(周四)下午 3 点」
    -""".strip()
    -
    -    result = builder.render(
    -        ContextRequest(
    -            history=[],
    -            current_message="你还记得明天什么时候面试吗",
    -            multimodal=True,
    -            channel="telegram",
    -            chat_id="7674283004",
    -            message_timestamp=request_time,
    -            turn_injection_prompt=turn_injection_prompt,
    -        )
    -    )
    -
    -    system_prompt = result.messages[0]["content"]
    -    context_frame = result.messages[-2]["content"]
    -    user_message = result.messages[-1]["content"]
    -
    -    assert "request_time=2026-04-08T17:57:00+08:00" not in system_prompt
    -    assert "local_date=2026-04-08" not in system_prompt
    -    assert "今天=2026-04-08" not in system_prompt
    -    assert "明天=2026-04-09" not in system_prompt
    -    assert context_frame.startswith(SYSTEM_CONTEXT_FRAME_MARKER)
    -    assert "用户表示明天下午三点有面试" in context_frame
    -    assert "准备次日下午三点的字节跳动面试" in context_frame
    -    assert "4 月 9 日(周四)下午 3 点" in context_frame
    -    assert user_message.startswith(
    -        f"[当前消息时间: {local_request_time:%Y-%m-%d %H:%M:%S}"
    -    )
    -    assert f"request_time={local_request_time.isoformat()}" in user_message
    -    assert f"今天={local_request_time:%Y-%m-%d}" in user_message
    -    assert f"昨天={local_request_time - timedelta(days=1):%Y-%m-%d}" in user_message
    -    assert f"明天={local_request_time + timedelta(days=1):%Y-%m-%d}" in user_message
    -    assert f"后天={local_request_time + timedelta(days=2):%Y-%m-%d}" in user_message
    -    assert f"weekday={local_request_time:%A}" in user_message
    -    assert "相对时间以此为准" in user_message
    -    assert user_message.endswith("你还记得明天什么时候面试吗")
    -
    -
    -@pytest.mark.asyncio
    -async def test_message_bus_rejects_removed_legacy_outbound_paths():
    -    bus = MessageBus()
    -    with pytest.raises(RuntimeError, match="legacy publish_outbound 已删除"):
    -        await bus.publish_outbound(OutboundMessage("telegram", "1", "payload"))
    -    with pytest.raises(RuntimeError, match="legacy publish_outbound_awaited 已删除"):
    -        await bus.publish_outbound_awaited(OutboundMessage("telegram", "1", "payload"))
    -    assert bus.inbound_size == 0
    -    assert bus.outbound_size == 0
    diff --git a/tests/test_telegram_utils.py b/tests/test_telegram_utils.py
    deleted file mode 100644
    index edda20e9d..000000000
    --- a/tests/test_telegram_utils.py
    +++ /dev/null
    @@ -1,663 +0,0 @@
    -import asyncio
    -import logging
    -from typing import Any, cast
    -from types import SimpleNamespace
    -
    -import pytest
    -from unittest.mock import AsyncMock
    -
    -from infra.channels.telegram_utils import (
    -    TelegramLiveEditQueue,
    -    TelegramLiveTextMessage,
    -    TelegramOutboundLimiter,
    -    TelegramStreamMessage,
    -    render_telegram_preview_html,
    -    send_markdown,
    -    send_stream_markdown,
    -    send_thinking_block,
    -)
    -from infra.channels import telegram_utils as telegram_utils_module
    -
    -
    -class BotStub:
    -    def __init__(self):
    -        self.messages = []
    -        self.edits = []
    -        self.document_calls = 0
    -        self.photo_calls = 0
    -
    -    async def send_message(self, **kwargs):
    -        self.messages.append(kwargs)
    -        return SimpleNamespace(message_id=len(self.messages))
    -
    -    async def edit_message_text(self, **kwargs):
    -        self.edits.append(kwargs)
    -
    -    async def send_document(self, **kwargs):
    -        self.document_calls += 1
    -
    -    async def send_photo(self, **kwargs):
    -        self.photo_calls += 1
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("channel", ["live", "preview"])
    -async def test_html_send_helper_falls_back_and_returns_message(channel, caplog):
    -    bot = BotStub()
    -    calls = []
    -    expected = SimpleNamespace(message_id=9)
    -
    -    async def send_message(**kwargs):
    -        calls.append(kwargs)
    -        if kwargs.get("parse_mode") == "HTML":
    -            raise RuntimeError("can't parse entities")
    -        return expected
    -
    -    bot.send_message = send_message
    -
    -    with caplog.at_level(logging.WARNING, logger=telegram_utils_module.logger.name):
    -        result = await telegram_utils_module._send_html_message(
    -            cast(Any, bot),
    -            123,
    -            "hello",
    -            "hello",
    -            channel=channel,
    -        )
    -
    -    assert result is expected
    -    assert (
    -        f"[telegram] {channel} HTML 解析失败,降级纯文本: can't parse entities"
    -        in caplog.text
    -    )
    -    assert calls == [
    -        {"chat_id": 123, "text": "hello", "parse_mode": "HTML"},
    -        {"chat_id": 123, "text": "hello"},
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("channel", ["live", "preview"])
    -async def test_html_send_helper_propagates_non_parse_error(channel):
    -    bot = BotStub()
    -    calls = []
    -
    -    async def send_message(**kwargs):
    -        calls.append(kwargs)
    -        raise RuntimeError("transport failed")
    -
    -    bot.send_message = send_message
    -
    -    with pytest.raises(RuntimeError, match="transport failed"):
    -        await telegram_utils_module._send_html_message(
    -            cast(Any, bot),
    -            123,
    -            "hello",
    -            "hello",
    -            channel=channel,
    -        )
    -
    -    assert calls == [{"chat_id": 123, "text": "hello", "parse_mode": "HTML"}]
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("channel", "expected_result"),
    -    [("live", True), ("preview", None)],
    -)
    -async def test_html_edit_helper_falls_back_with_channel_result(
    -    channel, expected_result, caplog
    -):
    -    bot = BotStub()
    -    calls = []
    -
    -    async def edit_message_text(**kwargs):
    -        calls.append(kwargs)
    -        if kwargs.get("parse_mode") == "HTML":
    -            raise RuntimeError("can't parse entities")
    -
    -    bot.edit_message_text = edit_message_text
    -
    -    with caplog.at_level(logging.WARNING, logger=telegram_utils_module.logger.name):
    -        result = await telegram_utils_module._edit_html_message(
    -            cast(Any, bot),
    -            123,
    -            9,
    -            "hello",
    -            "hello",
    -            channel=channel,
    -        )
    -
    -    assert result is expected_result
    -    assert calls == [
    -        {
    -            "chat_id": 123,
    -            "message_id": 9,
    -            "text": "hello",
    -            "parse_mode": "HTML",
    -        },
    -        {"chat_id": 123, "message_id": 9, "text": "hello"},
    -    ]
    -    assert f"[telegram] {channel} edit HTML 解析失败" in caplog.text
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize("channel", ["live", "preview"])
    -async def test_html_edit_helper_propagates_non_parse_error(channel):
    -    bot = BotStub()
    -    calls = []
    -
    -    async def edit_message_text(**kwargs):
    -        calls.append(kwargs)
    -        raise RuntimeError("transport failed")
    -
    -    bot.edit_message_text = edit_message_text
    -
    -    with pytest.raises(RuntimeError, match="transport failed"):
    -        await telegram_utils_module._edit_html_message(
    -            cast(Any, bot),
    -            123,
    -            9,
    -            "hello",
    -            "hello",
    -            channel=channel,
    -        )
    -
    -    assert calls == [
    -        {
    -            "chat_id": 123,
    -            "message_id": 9,
    -            "text": "hello",
    -            "parse_mode": "HTML",
    -        }
    -    ]
    -
    -
    -@pytest.mark.asyncio
    -@pytest.mark.parametrize(
    -    ("channel", "expected_result"),
    -    [("live", True), ("preview", None)],
    -)
    -async def test_html_edit_helper_skips_not_modified_without_plain_retry(
    -    channel, expected_result, caplog
    -):
    -    bot = BotStub()
    -    bot.edit_message_text = AsyncMock(
    -        side_effect=telegram_utils_module.BadRequest("Message is NOT modified")
    -    )
    -
    -    with caplog.at_level(logging.DEBUG, logger=telegram_utils_module.logger.name):
    -        result = await telegram_utils_module._edit_html_message(
    -            cast(Any, bot),
    -            123,
    -            9,
    -            "hello",
    -            "hello",
    -            channel=channel,
    -        )
    -
    -    assert result is expected_result
    -    bot.edit_message_text.assert_awaited_once_with(
    -        chat_id=123,
    -        message_id=9,
    -        text="hello",
    -        parse_mode="HTML",
    -    )
    -    if channel == "preview":
    -        assert "[telegram] preview edit skipped" in caplog.text
    -    else:
    -        assert "edit skipped" not in caplog.text
    -
    -
    -@pytest.mark.asyncio
    -async def test_send_markdown_splits_long_code_block_into_multiple_messages():
    -    bot = BotStub()
    -    code = "print('x')\n" * 800
    -    markdown = f"```python\n{code}```"
    -
    -    await send_markdown(cast(Any, bot), "123", markdown)
    -
    -    assert len(bot.messages) >= 2
    -    assert bot.document_calls == 0
    -    assert bot.photo_calls == 0
    -    assert all(call["chat_id"] == 123 for call in bot.messages)
    -    assert all(call["text"].strip() for call in bot.messages)
    -    assert any(entity["type"] == "pre" for entity in bot.messages[0]["entities"])
    -    assert all(len(call["text"]) <= 4090 for call in bot.messages)
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_limiter_retries_after_cooling_down(monkeypatch):
    -    from infra.channels import telegram_utils as mod
    -
    -    limiter = TelegramOutboundLimiter(
    -        send_interval_s=2.0,
    -        global_interval_s=0.0,
    -        retry_padding_s=1.0,
    -        max_attempts=2,
    -    )
    -    sleep_mock = AsyncMock()
    -    monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock)
    -    monkeypatch.setattr(
    -        "infra.channels.telegram_utils.asyncio.get_running_loop",
    -        lambda: SimpleNamespace(time=lambda: 100.0),
    -    )
    -    calls = 0
    -
    -    async def action():
    -        nonlocal calls
    -        calls += 1
    -        if calls == 1:
    -            raise mod.RetryAfter(cast(Any, 3.0))
    -        return "ok"
    -
    -    result = await limiter.run(123, kind="send", label="send_message(test)", action=action)
    -
    -    assert result == "ok"
    -    assert calls == 2
    -    assert sleep_mock.await_args_list[0].args[0] == 4.0
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_limiter_typing_does_not_delay_send(monkeypatch):
    -    limiter = TelegramOutboundLimiter(
    -        send_interval_s=2.0,
    -        typing_interval_s=8.0,
    -        global_interval_s=0.0,
    -    )
    -    sleep_mock = AsyncMock()
    -    monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock)
    -
    -    await limiter.run(123, kind="typing", label="typing", action=AsyncMock(return_value=True))
    -    await limiter.run(123, kind="send", label="send", action=AsyncMock(return_value=True))
    -
    -    sleep_mock.assert_not_awaited()
    -
    -
    -@pytest.mark.asyncio
    -async def test_lock_maps_allocate_only_for_missing_keys(monkeypatch):
    -    original_lock = telegram_utils_module.asyncio.Lock
    -    constructions = 0
    -
    -    def counting_lock():
    -        nonlocal constructions
    -        constructions += 1
    -        return original_lock()
    -
    -    limiter = TelegramOutboundLimiter(
    -        send_interval_s=0.0,
    -        typing_interval_s=0.0,
    -        global_interval_s=0.0,
    -    )
    -    existing_chat_lock = original_lock()
    -    existing_typing_lock = original_lock()
    -    limiter._chat_locks[123] = existing_chat_lock
    -    limiter._typing_locks[123] = existing_typing_lock
    -    queue = TelegramLiveEditQueue(min_interval_s=0.0)
    -    existing_queue_lock = original_lock()
    -    queue._locks[123] = existing_queue_lock
    -
    -    monkeypatch.setattr(telegram_utils_module.asyncio, "Lock", counting_lock)
    -    await limiter.run(123, kind="send", label="send", action=AsyncMock(return_value=True))
    -    await limiter.run(123, kind="typing", label="typing", action=AsyncMock(return_value=True))
    -    await queue.reserve(123, label="existing")
    -    await queue.run(123, label="existing", action=AsyncMock(return_value=True))
    -
    -    assert constructions == 0
    -
    -    await limiter.run(456, kind="send", label="send", action=AsyncMock(return_value=True))
    -    await limiter.run(456, kind="typing", label="typing", action=AsyncMock(return_value=True))
    -    await queue.reserve(456, label="missing")
    -    await queue.run(789, label="missing", action=AsyncMock(return_value=True))
    -
    -    assert constructions == 4
    -    assert limiter._chat_locks[123] is existing_chat_lock
    -    assert limiter._typing_locks[123] is existing_typing_lock
    -    assert queue._locks[123] is existing_queue_lock
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_limiter_global_slot_covers_action():
    -    limiter = TelegramOutboundLimiter(
    -        send_interval_s=0.0,
    -        global_interval_s=0.0,
    -    )
    -    first_started = asyncio.Event()
    -    release_first = asyncio.Event()
    -    second_started = asyncio.Event()
    -
    -    async def first_action():
    -        first_started.set()
    -        await release_first.wait()
    -        return "first"
    -
    -    async def second_action():
    -        second_started.set()
    -        return "second"
    -
    -    first_task = asyncio.create_task(
    -        limiter.run(123, kind="send", label="first", action=first_action)
    -    )
    -    await first_started.wait()
    -    second_task = asyncio.create_task(
    -        limiter.run(456, kind="send", label="second", action=second_action)
    -    )
    -    await asyncio.sleep(0)
    -
    -    assert not second_started.is_set()
    -    release_first.set()
    -    assert await first_task == "first"
    -    assert await second_task == "second"
    -
    -
    -@pytest.mark.asyncio
    -async def test_outbound_limiter_typing_retry_after_sets_cooldown(monkeypatch):
    -    from infra.channels import telegram_utils as mod
    -
    -    limiter = TelegramOutboundLimiter(
    -        typing_interval_s=8.0,
    -        retry_padding_s=1.0,
    -    )
    -    sleep_mock = AsyncMock()
    -    monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock)
    -
    -    with pytest.raises(mod.RetryAfter):
    -        await limiter.run(
    -            123,
    -            kind="typing",
    -            label="typing",
    -            action=AsyncMock(side_effect=mod.RetryAfter(cast(Any, 3.0))),
    -        )
    -    await limiter.run(123, kind="typing", label="typing", action=AsyncMock(return_value=True))
    -
    -    assert sleep_mock.await_args_list[0].args[0] >= 3.9
    -
    -
    -@pytest.mark.asyncio
    -async def test_send_markdown_does_not_fallback_when_send_fails(monkeypatch):
    -    from infra.channels import telegram_utils as mod
    -
    -    bot = BotStub()
    -    bot.send_message = AsyncMock(side_effect=mod.TimedOut("x"))
    -
    -    with pytest.raises(mod.TimedOut):
    -        await send_markdown(cast(Any, bot), 123, "hello")
    -
    -    assert bot.send_message.await_count == 3
    -
    -
    -@pytest.mark.asyncio
    -async def test_send_markdown_falls_back_to_plain_text(monkeypatch):
    -    bot = BotStub()
    -
    -    def fake_convert_with_segments(text):
    -        raise TypeError("boom")
    -
    -    monkeypatch.setattr(
    -        "infra.channels.telegram_utils.convert_with_segments", fake_convert_with_segments
    -    )
    -
    -    await send_markdown(cast(Any, bot), 456, "line1\nline2")
    -
    -    assert bot.messages == [{"chat_id": 456, "text": "line1\nline2"}]
    -
    -
    -@pytest.mark.asyncio
    -async def test_plain_text_fallback_respects_utf16_message_limit(monkeypatch):
    -    bot = BotStub()
    -
    -    monkeypatch.setattr(
    -        telegram_utils_module,
    -        "convert_with_segments",
    -        lambda text: (_ for _ in ()).throw(TypeError("boom")),
    -    )
    -
    -    await send_markdown(cast(Any, bot), 456, "😀" * 3000)
    -
    -    assert len(bot.messages) == 2
    -    assert all(
    -        len(message["text"].encode("utf-16-le")) // 2 <= 4090
    -        for message in bot.messages
    -    )
    -
    -
    -def test_plain_text_split_rejects_character_larger_than_limit() -> None:
    -    with pytest.raises(ValueError, match="单个字符超过"):
    -        telegram_utils_module._split_text("😀", 1)
    -
    -
    -def test_render_telegram_preview_html_renders_markdown():
    -    html = render_telegram_preview_html("### 标题\n\n**重点**\n\n- 一\n- 二")
    -    assert "标题" in html
    -    assert "重点" in html
    -    assert "• 一" in html
    -    assert "• 二" in html
    -
    -
    -def test_render_telegram_preview_html_supports_links_strike_and_spoiler():
    -    html = render_telegram_preview_html("[官网](https://example.com) 和 ~~删除~~ 以及 ||隐藏||")
    -    assert '' in html
    -    assert "删除" in html
    -    assert "隐藏" in html
    -
    -
    -def test_render_telegram_preview_html_keeps_spacing_compact():
    -    html = render_telegram_preview_html(
    -        "我在。\n\n### 呼吸\n\n1. 吸气\n\n1. 呼气\n\n> 慢一点"
    -    )
    -    assert "呼吸" in html
    -    assert "• 吸气" in html
    -    assert "• 呼气" in html
    -    assert "
    慢一点
    " in html - assert "\n\n\n" not in html - - -@pytest.mark.asyncio -async def test_stream_message_falls_back_to_plain_text_on_html_parse_error(): - bot = BotStub() - - async def broken_edit_message_text(**kwargs): - if kwargs.get("parse_mode") == "HTML": - raise RuntimeError("can't parse entities") - bot.edits.append(kwargs) - - bot.edit_message_text = broken_edit_message_text - stream = TelegramStreamMessage(cast(Any, bot), 123) - await stream.push_delta("**hello**") - await stream.finalize("**hello**\n\n- a\n- b") - - assert bot.messages[0]["parse_mode"] == "HTML" - assert bot.edits[-1]["text"] == "**hello**\n\n- a\n- b" - - -@pytest.mark.asyncio -async def test_stream_preview_clips_utf16_text() -> None: - bot = BotStub() - stream = TelegramStreamMessage(cast(Any, bot), 123) - - await stream.push_delta("😀" * 3000, force=True) - - assert len(stream._last_sent_plain.encode("utf-16-le")) // 2 <= 4096 - - -@pytest.mark.asyncio -async def test_send_stream_markdown_falls_back_to_markdown_on_stream_failure(): - bot = BotStub() - bot.edit_message_text = AsyncMock(side_effect=RuntimeError("boom")) - - text = "hello world " * 30 - await send_stream_markdown(cast(Any, bot), 123, text) - - assert len(bot.messages) == 2 - assert bot.messages[-1]["text"] == text - assert bot.edit_message_text.await_count == 1 - - -@pytest.mark.asyncio -async def test_stream_message_ignores_message_not_modified_error(): - bot = BotStub() - - class MessageNotModifiedError(Exception): - pass - - async def unchanged_edit_message_text(**kwargs): - raise MessageNotModifiedError( - "Message is not modified: specified new message content and reply markup " - "are exactly the same as a current content and reply markup of the message" - ) - - bot.edit_message_text = unchanged_edit_message_text - stream = TelegramStreamMessage(cast(Any, bot), 123) - - await stream.push_delta("hello") - await stream.finalize("hello") - - assert len(bot.messages) == 1 - - -@pytest.mark.asyncio -async def test_stream_message_skips_duplicate_truncated_preview(): - bot = BotStub() - stream = TelegramStreamMessage(cast(Any, bot), 123) - first = "a" * 4096 + "X" - second = "a" * 4096 + "Y" - - await stream.push_delta(first, force=True) - await stream.finalize(second) - - assert len(bot.messages) == 1 - assert bot.edits == [] - - -@pytest.mark.asyncio -async def test_stream_message_retry_after_enters_cooldown_without_blocking(monkeypatch): - bot = BotStub() - from infra.channels import telegram_utils as mod - - values = [10.0, 20.0, 30.0, 70.0, 80.0] - - class _Loop: - def __init__(self): - self._index = 0 - - def time(self): - value = values[min(self._index, len(values) - 1)] - self._index += 1 - return value - - async def limited_edit_message_text(**kwargs): - raise mod.RetryAfter(cast(Any, 48.0)) - - bot.edit_message_text = limited_edit_message_text - sleep_mock = AsyncMock() - monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock) - monkeypatch.setattr( - "infra.channels.telegram_utils.asyncio.get_running_loop", - lambda: _Loop(), - ) - - stream = TelegramStreamMessage(cast(Any, bot), 123) - await stream.push_delta("hello", force=True) - await stream.push_delta(" world", force=True) - assert stream._edit_cooldown_until > 30.0 - await stream.push_delta(" again") - await stream.push_delta(" after cooldown") - - assert sleep_mock.await_count == 0 - assert len(bot.messages) == 1 - assert len(bot.edits) == 0 - - -@pytest.mark.asyncio -async def test_live_edit_queue_backoff_and_force_retry(monkeypatch): - from infra.channels import telegram_utils as mod - - class _Loop: - def __init__(self): - self._now = 10.0 - - def time(self): - self._now += 1.0 - return self._now - - bot = BotStub() - sleep_mock = AsyncMock() - loop = _Loop() - monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock) - monkeypatch.setattr( - "infra.channels.telegram_utils.asyncio.get_running_loop", - lambda: loop, - ) - queue = TelegramLiveEditQueue(min_interval_s=1.0) - live = TelegramLiveTextMessage(cast(Any, bot), queue, 123) - - await live.update("hello") - sleep_mock.reset_mock() - bot.edit_message_text = AsyncMock(side_effect=mod.RetryAfter(cast(Any, 8.0))) - await live.update("hello world") - assert queue._flood_strikes[123] == 1 - assert queue._current_interval_s[123] == 2.0 - assert sleep_mock.await_count == 0 - - bot.edit_message_text = AsyncMock(side_effect=mod.RetryAfter(cast(Any, 1.0))) - await live.update("hello world again") - assert queue._flood_strikes[123] >= 3 - - bot.edit_message_text = AsyncMock(return_value=True) - await live.update("hello world final") - assert bot.edit_message_text.await_count == 0 - await live.update("hello world final", force=True) - assert bot.edit_message_text.await_count == 1 - assert queue._flood_strikes[123] == 0 - - -@pytest.mark.asyncio -async def test_live_edit_queue_with_limiter_skips_retry_after_frame(monkeypatch): - from infra.channels import telegram_utils as mod - - bot = BotStub() - bot.edit_message_text = AsyncMock(side_effect=mod.RetryAfter(cast(Any, 8.0))) - sleep_mock = AsyncMock() - monkeypatch.setattr("infra.channels.telegram_utils.asyncio.sleep", sleep_mock) - limiter = TelegramOutboundLimiter( - send_interval_s=0.0, - edit_interval_s=0.0, - global_interval_s=0.0, - retry_padding_s=0.0, - ) - queue = TelegramLiveEditQueue(min_interval_s=0.0, limiter=limiter) - live = TelegramLiveTextMessage(cast(Any, bot), queue, 123) - live._message_id = 1 - live._last_plain = "old" - - await live.update("new") - - assert bot.edit_message_text.await_count == 1 - sleep_mock.assert_not_awaited() - assert queue._flood_strikes[123] == 1 - - -@pytest.mark.asyncio -async def test_send_thinking_block_splits_long_content(): - bot = BotStub() - # 每个中文字符占 1 个 UTF-16 code unit,构造超长 thinking - thinking = "思" * 5000 - await send_thinking_block(cast(Any, bot), 123, thinking) - assert len(bot.messages) >= 2 - # 每条消息都应该有 expandable_blockquote entity - for msg in bot.messages: - entities = msg.get("entities", []) - assert len(entities) == 1 - assert entities[0].type == "expandable_blockquote" - # 第一条包含 header - assert bot.messages[0]["text"].startswith("💭 思考过程") - # 拼合所有 text 应还原完整内容 - combined = "".join(m["text"] for m in bot.messages) - assert "思" * 5000 in combined - - -@pytest.mark.asyncio -async def test_send_thinking_block_short_content_single_message(): - bot = BotStub() - await send_thinking_block(cast(Any, bot), 123, "短思考") - assert len(bot.messages) == 1 - assert "短思考" in bot.messages[0]["text"] diff --git a/tests/test_time_parsing.py b/tests/test_time_parsing.py deleted file mode 100644 index db35e2542..000000000 --- a/tests/test_time_parsing.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Tests for duration and datetime parsing utilities.""" - -from datetime import datetime, timedelta, timezone - -import pytest - -from agent.scheduler import is_cron_expr, next_cron_fire, parse_duration, parse_when_at - - -class TestParseDuration: - def test_seconds(self): - assert parse_duration("30s") == timedelta(seconds=30) - - def test_minutes(self): - assert parse_duration("5m") == timedelta(minutes=5) - - def test_hours(self): - assert parse_duration("2h") == timedelta(hours=2) - - def test_days(self): - assert parse_duration("1d") == timedelta(days=1) - - def test_compound_hours_minutes(self): - assert parse_duration("1h30m") == timedelta(hours=1, minutes=30) - - def test_compound_all(self): - assert parse_duration("1d2h30m15s") == timedelta( - days=1, hours=2, minutes=30, seconds=15 - ) - - def test_invalid_raises(self): - with pytest.raises(ValueError, match="无效的时间间隔"): - parse_duration("abc") - - def test_empty_raises(self): - with pytest.raises(ValueError): - parse_duration("") - - -class TestParseWhenAt: - def test_hhmm_future_today(self): - # 13:00 is in the future relative to 12:00 - from zoneinfo import ZoneInfo - - tz = "Asia/Shanghai" - ref = datetime(2025, 6, 1, 12, 0, 0, tzinfo=ZoneInfo(tz)) - result = parse_when_at("13:00", tz, _now_fn=lambda: ref) - assert result.hour == 13 - assert result.minute == 0 - assert result.date() == ref.date() - - def test_hhmm_past_advances_to_tomorrow(self): - from zoneinfo import ZoneInfo - - tz = "Asia/Shanghai" - ref = datetime(2025, 6, 1, 14, 0, 0, tzinfo=ZoneInfo(tz)) - result = parse_when_at("09:00", tz, _now_fn=lambda: ref) - assert result.day == 2 # tomorrow - - def test_iso_datetime(self): - result = parse_when_at("2025-06-01T14:30:00", "UTC") - assert result.year == 2025 - assert result.month == 6 - assert result.day == 1 - assert result.hour == 14 - assert result.minute == 30 - - def test_iso_with_offset(self): - result = parse_when_at("2025-06-01T14:30:00+08:00", "UTC") - assert result.utcoffset() is not None - - def test_timezone_applied_to_hhmm(self): - from zoneinfo import ZoneInfo - - tz = "Asia/Shanghai" - ref = datetime(2025, 6, 1, 8, 0, 0, tzinfo=ZoneInfo(tz)) - result = parse_when_at("09:00", tz, _now_fn=lambda: ref) - assert result.tzinfo is not None - - def test_hhmm_normalizes_injected_clock_to_requested_timezone(self): - from zoneinfo import ZoneInfo - - ref_utc = datetime(2025, 6, 1, 0, 0, 0, tzinfo=timezone.utc) - result = parse_when_at( - "09:00", "Asia/Shanghai", _now_fn=lambda: ref_utc - ) - assert result == datetime(2025, 6, 1, 9, 0, tzinfo=ZoneInfo("Asia/Shanghai")) - - def test_invalid_raises(self): - with pytest.raises(ValueError, match="无法解析时间"): - parse_when_at("not-a-time", "UTC") - - -class TestIsCronExpr: - def test_valid_cron_5_fields(self): - assert is_cron_expr("0 9 * * *") is True - - def test_valid_cron_with_wildcards(self): - assert is_cron_expr("*/5 * * * *") is True - - def test_interval_string_not_cron(self): - assert is_cron_expr("1h") is False - - def test_duration_not_cron(self): - assert is_cron_expr("30s") is False - - def test_four_fields_not_cron(self): - assert is_cron_expr("0 9 * *") is False - - -class TestNextCronFire: - def test_fixed_daily_cron_returns_next_boundary(self): - after = datetime(2025, 6, 1, 8, 0, 1, tzinfo=timezone.utc) - result = next_cron_fire("0 9 * * *", "UTC", after) - assert result == datetime(2025, 6, 1, 9, 0, 0, tzinfo=timezone.utc) - - def test_step_cron_advances_to_next_match(self): - after = datetime(2025, 6, 1, 8, 1, 0, tzinfo=timezone.utc) - result = next_cron_fire("*/5 * * * *", "UTC", after) - assert result == datetime(2025, 6, 1, 8, 5, 0, tzinfo=timezone.utc) - - def test_six_field_cron_supports_seconds(self): - after = datetime(2025, 6, 1, 8, 0, 0, tzinfo=timezone.utc) - result = next_cron_fire("*/5 * * * * *", "UTC", after) - assert result == datetime(2025, 6, 1, 8, 0, 5, tzinfo=timezone.utc) - - @pytest.mark.parametrize( - ("cron_expr", "expected"), - [ - ("0 9 * * 0", datetime(2025, 6, 2, 9, tzinfo=timezone.utc)), - ("0 0 9 * * 0", datetime(2025, 6, 2, 9, tzinfo=timezone.utc)), - ], - ) - def test_numeric_cron_weekday_uses_apscheduler_weekday_zero( - self, cron_expr, expected - ): - after = datetime(2025, 6, 2, 8, tzinfo=timezone.utc) - assert next_cron_fire(cron_expr, "UTC", after) == expected - - @pytest.mark.parametrize("cron_expr", ["0 9 * *", "0 0 0 1 1 * *"]) - def test_invalid_cron_field_count_raises(self, cron_expr): - with pytest.raises(ValueError, match="无效的 cron 表达式"): - next_cron_fire( - cron_expr, - "UTC", - datetime(2025, 6, 1, 8, tzinfo=timezone.utc), - ) diff --git a/tests/test_tool_discovery_routing.py b/tests/test_tool_discovery_routing.py deleted file mode 100644 index 3f78a9157..000000000 --- a/tests/test_tool_discovery_routing.py +++ /dev/null @@ -1,82 +0,0 @@ -"""未知工具调用的恢复提示合同。""" - -from __future__ import annotations - -import asyncio -from pathlib import Path -from typing import Any -from unittest.mock import MagicMock - -from agent.context import ContextBuilder -from agent.looping.core import AgentLoop -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps, LLMConfig -from agent.plugin_composition import LLMResponse, ToolCall -from agent.tools.registry import ToolRegistry -from agent.tools.tool_search import ToolSearchTool -from bus.queue import MessageBus -from tests.compaction_fakes import run_test_agent_loop -from tests.memory_fakes import FakeMemoryEngine -from tests.provider_fakes import ProviderContextBudgetStub - - -class _FakeProvider(ProviderContextBudgetStub): - def __init__(self, responses: list[LLMResponse]) -> None: - self._responses = list(responses) - - async def chat(self, **kwargs: Any) -> LLMResponse: - if not self._responses: - raise AssertionError("provider.chat 被调用次数超过预期") - return self._responses.pop(0) - - -def _make_loop( - tmp_path: Path, - provider: _FakeProvider, - registry: ToolRegistry, -) -> AgentLoop: - return AgentLoop( - AgentLoopDeps( - bus=MessageBus(), - tools=registry, - session_manager=MagicMock(), - workspace=tmp_path, - context=ContextBuilder(tmp_path), - ), - AgentLoopConfig(llm=LLMConfig(max_iterations=10, tool_search_enabled=True)), - ) - - -def test_unknown_tool_error_contains_recovery_query(tmp_path: Path) -> None: - registry = ToolRegistry() - registry.register( - ToolSearchTool(registry), - always_on=True, - risk="read-only", - ) - provider = _FakeProvider( - [ - LLMResponse( - content="", - tool_calls=[ToolCall("c1", "rss_manage", {})], - ), - LLMResponse(content="好的", tool_calls=[]), - ] - ) - - _, _, tool_chain, _, _ = asyncio.run( - run_test_agent_loop( - _make_loop(tmp_path, provider, registry), - provider, - [{"role": "user", "content": "管理RSS"}], - ) - ) - - calls = [ - call - for step in tool_chain - for call in step.get("calls", []) - if call["name"] == "rss_manage" - ] - assert len(calls) == 1 - assert "select:rss_manage" in calls[0]["result"] - assert "tool_search" in calls[0]["result"] diff --git a/tests/test_tool_discovery_state.py b/tests/test_tool_discovery_state.py deleted file mode 100644 index 80d090f9b..000000000 --- a/tests/test_tool_discovery_state.py +++ /dev/null @@ -1,35 +0,0 @@ -from agent.core.runtime_support import ToolDiscoveryState - - -def test_tool_discovery_state_keeps_most_recent_tools(): - state = ToolDiscoveryState(capacity=2) - state.update("cli:1", ["tool_a", "tool_b"], {"always"}) - assert state.get_preloaded_ordered("cli:1") == ["tool_a", "tool_b"] - - state.update("cli:1", ["tool_a"], {"always"}) - state.update("cli:1", ["tool_c"], {"always"}) - - assert state.get_preloaded_ordered("cli:1") == ["tool_a", "tool_c"] - - -def test_tool_discovery_state_skips_always_on_and_tool_search(): - state = ToolDiscoveryState() - state.update( - "cli:1", ["always_tool", "tool_search", "hidden_tool"], {"always_tool"} - ) - - assert state.get_preloaded_ordered("cli:1") == ["hidden_tool"] - - -def test_tool_discovery_state_bounds_session_cache(): - state = ToolDiscoveryState(session_capacity=2) - state.update("cli:1", ["tool_a"], set()) - state.update("cli:2", ["tool_b"], set()) - - assert state.get_preloaded_ordered("cli:1") == ["tool_a"] - - state.update("cli:3", ["tool_c"], set()) - - assert state.get_preloaded_ordered("cli:2") == [] - assert state.get_preloaded_ordered("cli:1") == ["tool_a"] - assert state.get_preloaded_ordered("cli:3") == ["tool_c"] diff --git a/tests/test_tool_search.py b/tests/test_tool_search.py deleted file mode 100644 index b7a0f8f10..000000000 --- a/tests/test_tool_search.py +++ /dev/null @@ -1,807 +0,0 @@ -""" -tool_search 搜索质量回归测试。 - -覆盖场景: -- 工具 name / description 自动索引,无需手写 search_hint -- CJK bigram 归一化:中文查询无需分词库 -- risk 过滤 -- MCP 工具能被搜索到 -- baseline 回归(从 tests/fixtures/tool_search_baseline.json 加载) -""" - -import asyncio -import json -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import MagicMock - - -import pytest - -from agent.mcp.client import McpToolInfo -from agent.mcp.tool import McpToolWrapper -from agent.tools.base import Tool -from agent.tools.registry import ToolDocument, ToolRegistry -from agent.tools.search_backend import KeywordSearchBackend, _default_normalize -from agent.tools.tool_search import ToolSearchTool - -# ── 辅助工具桩 ──────────────────────────────────────────────────────────────── - - -class _StubTool(Tool): - def __init__(self, name: str, description: str, params: dict | None = None) -> None: - self._name = name - self._description = description - self._params = params or {"type": "object", "properties": {}} - - @property - def name(self) -> str: - return self._name - - @property - def description(self) -> str: - return self._description - - @property - def parameters(self) -> dict[str, Any]: - return self._params - - async def execute(self, **kwargs: Any) -> str: - return "ok" - - -class _PathOnlyTool(Tool): - name = "path_only" - description = "测试只接收 path 的工具" - parameters = { - "type": "object", - "properties": { - "path": {"type": "string", "description": "路径"}, - }, - "required": ["path"], - } - - async def execute(self, path: str) -> str: - return path - - -class _ExistingDescriptionTool(Tool): - name = "existing_description" - description = "测试自带 description 参数的工具" - parameters = { - "type": "object", - "properties": { - "description": {"type": "string", "description": "业务描述"}, - }, - "required": ["description"], - } - - async def execute(self, description: str) -> str: - return description - - -def _make_registry() -> ToolRegistry: - """构建测试用 registry。 - - 描述比原来更丰富,以覆盖各种中文查询 —— - 不依赖手写 search_hint 或同义词表。 - """ - reg = ToolRegistry() - reg.register( - ToolSearchTool(reg), - always_on=True, - risk="read-only", - ) - reg.register( - _StubTool("write_file", "将内容写入指定文件路径,可用于保存、创建文件"), - risk="write", - ) - reg.register( - _StubTool("edit_file", "编辑或修改已有文件的指定行"), - risk="write", - ) - reg.register( - _StubTool("list_dir", "列出目录下的文件和子目录,即 ls 命令"), - risk="read-only", - ) - reg.register( - _StubTool("read_file", "读取或查看文件内容"), - risk="read-only", - always_on=True, - ) - reg.register( - _StubTool( - "schedule", - "创建定时任务或提醒,在指定时间自动执行动作(cron)", - params={ - "type": "object", - "properties": { - "cron": {"description": "cron 表达式,例如 * * * * *"}, - "action": {"description": "到时间后执行的动作描述"}, - }, - }, - ), - risk="write", - ) - reg.register( - _StubTool("feed_manage", "管理 RSS 订阅源,支持添加、删除、查询订阅"), - risk="write", - ) - reg.register( - _StubTool( - "mcp_fitbit__fitbit_health_snapshot", - "[Fitbit] 获取健康快照:步数、心率、运动数据等", - ), - risk="read-only", - source_type="mcp", - source_name="fitbit", - ) - reg.register( - _StubTool("message_push", "向用户推送或发送一条消息通知"), - risk="external-side-effect", - ) - reg.register( - _StubTool("memorize", "将信息存入长期记忆或备忘录"), - risk="write", - ) - reg.register( - _StubTool("web_search", "在互联网上搜索信息"), - risk="read-only", - always_on=True, - ) - return reg - - -def test_registry_adds_model_description_for_progress() -> None: - reg = ToolRegistry() - reg.register(_PathOnlyTool()) - - schema = reg.get_schemas(names={"path_only"})[0]["function"]["parameters"] - - assert "description" in schema["properties"] - assert "description" in schema["required"] - assert "description" not in _PathOnlyTool.parameters["properties"] - - -def test_registry_get_schemas_preserves_explicit_name_order() -> None: - reg = ToolRegistry() - reg.register(_StubTool("hidden_first", "先注册的隐藏工具")) - reg.register(_StubTool("always_later", "后注册的常驻工具"), always_on=True) - - schemas = reg.get_schemas(names=["always_later", "hidden_first"]) - - assert [schema["function"]["name"] for schema in schemas] == [ - "always_later", - "hidden_first", - ] - - -@pytest.mark.asyncio -async def test_registry_strips_progress_description_before_execute() -> None: - reg = ToolRegistry() - reg.register(_PathOnlyTool()) - - result = await reg.execute( - "path_only", - {"path": "/tmp/a", "description": "读取文件"}, - ) - - assert result == "/tmp/a" - - -@pytest.mark.asyncio -async def test_registry_keeps_real_description_parameter() -> None: - reg = ToolRegistry() - reg.register(_ExistingDescriptionTool()) - - result = await reg.execute( - "existing_description", - {"description": "业务描述"}, - ) - - assert result == "业务描述" - - -def test_search_reads_live_tool_document_fields_after_mutation() -> None: - backend = KeywordSearchBackend() - document = ToolDocument( - name="mutable_tool", - description="legacyalpha", - risk="read-only", - always_on=False, - search_hint=None, - source_type="builtin", - source_name="", - ) - backend.add(document) - - assert backend.search("legacyalpha")[0]["name"] == "mutable_tool" - - document.description = "freshbeta" - document.search_hint = "aliasomega" - - assert backend.search("legacyalpha") == [] - assert backend.search("freshbeta")[0]["summary"] == "freshbeta" - assert backend.search("aliasomega")[0]["why_matched"] == ["提示:aliasomega"] - - -# ── _default_normalize 单元测试 ─────────────────────────────────────────────── - - -class TestDefaultNormalize: - """验证 CJK bigram 归一化行为,无需同义词表即可中文召回。""" - - def test_chinese_bigrams(self): - result = _default_normalize("定时提醒") - assert "定时" in result # bigram - assert "提醒" in result # bigram - assert "定时提醒" in result # 原始串 - - def test_chinese_unigrams(self): - result = _default_normalize("目录") - assert "目" in result - assert "录" in result - assert "目录" in result - - def test_english_space_split(self): - result = _default_normalize("web search") - assert "web" in result - assert "search" in result - - def test_mixed(self): - result = _default_normalize("RSS订阅") - assert "rss" in result # lowercase - assert "订阅" in result # CJK bigram - assert "订" in result # unigram - - def test_original_preserved(self): - result = _default_normalize("schedule") - assert "schedule" in result - - -# ── ToolRegistry.search 集成测试 ────────────────────────────────────────────── - - -class TestRegistrySearch: - @pytest.fixture - def reg(self) -> ToolRegistry: - return _make_registry() - - def _names(self, results: list[dict]) -> list[str]: - return [r["name"] for r in results] - - # 核心场景:基于描述自动召回,无 search_hint - def test_文件写入(self, reg): - assert "write_file" in self._names(reg.search("文件写入")) - - def test_编辑文件(self, reg): - assert "edit_file" in self._names(reg.search("编辑文件")) - - def test_查看目录(self, reg): - assert "list_dir" in self._names(reg.search("查看目录")) - - def test_rss订阅(self, reg): - assert "feed_manage" in self._names(reg.search("RSS订阅")) - - def test_健康数据(self, reg): - assert "mcp_fitbit__fitbit_health_snapshot" in self._names(reg.search("健康数据")) - - def test_推送消息(self, reg): - assert "message_push" in self._names(reg.search("推送消息给用户")) - - def test_定时任务(self, reg): - assert "schedule" in self._names(reg.search("定时任务")) - - def test_设置提醒(self, reg): - # 依赖描述中有"提醒",不依赖同义词表 - assert "schedule" in self._names(reg.search("设置提醒")) - - def test_记忆(self, reg): - assert "memorize" in self._names(reg.search("记忆存储")) - - # 中文单字也能通过 bigram 召回 - def test_单字_目录(self, reg): - assert "list_dir" in self._names(reg.search("目录")) - - def test_单字_推送(self, reg): - assert "message_push" in self._names(reg.search("推送")) - - def test_单字_订阅(self, reg): - assert "feed_manage" in self._names(reg.search("订阅")) - - # cron 在工具描述中,可被召回 - def test_cron_in_description(self, reg): - results = reg.search("cron") - assert "schedule" in self._names(results) - - # tool_search 自身不出现在结果中 - def test_tool_search_excluded(self, reg): - results = reg.search("搜索工具") - assert all(r["name"] != "tool_search" for r in results) - - # top_k 限制 - def test_top_k(self, reg): - results = reg.search("文件", top_k=2) - assert len(results) <= 2 - - def test_top_k_preserves_score_and_name_tie_break(self): - reg = ToolRegistry() - for name in ("zeta_tool", "alpha_tool", "beta_tool"): - reg.register(_StubTool(name, "共享关键词")) - - results = reg.search("共享关键词", top_k=2) - - assert [item["name"] for item in results] == [ - "alpha_tool", - "beta_tool", - ] - - # risk 过滤 - def test_risk_filter_read_only(self, reg): - results = reg.search("文件", allowed_risk=["read-only"]) - for r in results: - assert r["risk"] == "read-only" - - def test_risk_filter_excludes_write(self, reg): - results = reg.search("文件写入", allowed_risk=["read-only"]) - names = self._names(results) - assert "write_file" not in names - - # 描述质量高的工具应排在前面 - def test_best_match_ranks_first(self, reg): - # "写文件" 最匹配 write_file(描述含"写入"+"文件") - results = reg.search("写文件") - assert len(results) > 0 - assert results[0]["name"] == "write_file" - - # why_matched 字段存在 - def test_why_matched_populated(self, reg): - results = reg.search("定时任务") - assert results - assert results[0]["why_matched"] - - # always_on 字段存在于结果中 - def test_always_on_field_present(self, reg): - results = reg.search("文件") - assert all("always_on" in r for r in results) - - -# ── MCP 工具可被搜索 ────────────────────────────────────────────────────────── - - -class TestMcpToolSearch: - def test_mcp_tool_discoverable_by_capability(self): - reg = ToolRegistry() - client = MagicMock() - client.name = "calendar" - info = McpToolInfo( - name="create_event", - description="Create a calendar event with title and time", - input_schema={"type": "object", "properties": {"title": {}, "time": {}}}, - ) - wrapper = McpToolWrapper(client, info) - - reg.register( - wrapper, - risk="external-side-effect", - source_type="mcp", - source_name="calendar", - ) - - results = reg.search("calendar") - assert any(r["name"] == "mcp_calendar__create_event" for r in results) - - results2 = reg.search("create event") - assert any(r["name"] == "mcp_calendar__create_event" for r in results2) - - def _make_feed_registry(self) -> ToolRegistry: - """模拟真实 feed MCP 工具注册(含中文 docstring)。""" - reg = ToolRegistry() - client = MagicMock() - client.name = "feed" - - # feed_manage:与 mcp_bridge.py 真实 docstring 对齐 - info_manage = McpToolInfo( - name="feed_manage", - description="管理 RSS 订阅源:添加、删除、列出订阅。支持 rss add / 添加订阅 / 订阅管理 / 取消订阅。", - input_schema={ - "type": "object", - "properties": { - "action": {"description": "list / add / remove"}, - "name": {}, - "url": {}, - }, - }, - ) - wrapper_manage = McpToolWrapper(client, info_manage) - reg.register( - wrapper_manage, - risk="external-side-effect", - source_type="mcp", - source_name="feed", - ) - - # feed_query:与 mcp_bridge.py 真实 docstring 对齐 - info_query = McpToolInfo( - name="feed_query", - description="查询 RSS 订阅内容,获取最近新闻、最新文章、最新资讯、rss查询。", - input_schema={ - "type": "object", - "properties": { - "action": {"description": "latest / search / sources"}, - "keyword": {}, - }, - }, - ) - wrapper_query = McpToolWrapper(client, info_query) - reg.register( - wrapper_query, - risk="external-side-effect", - source_type="mcp", - source_name="feed", - ) - return reg - - def test_feed_manage_chinese_discovery(self): - """S4 场景:中文 RSS 订阅管理发现路径(无手写同义词表)。""" - reg = self._make_feed_registry() - for query in ["RSS订阅", "添加订阅", "订阅管理"]: - names = [r["name"] for r in reg.search(query)] - assert "mcp_feed__feed_manage" in names, f"query={query!r} 未找到 feed_manage" - - def test_feed_query_chinese_discovery(self): - """中文新闻/最新资讯查询发现路径。""" - reg = self._make_feed_registry() - for query in ["最近新闻", "最新资讯"]: - names = [r["name"] for r in reg.search(query)] - assert "mcp_feed__feed_query" in names, f"query={query!r} 未找到 feed_query" - - def test_feed_manage_rss_add_selfheal(self): - """S5 场景:rss_add 废弃工具 → query hint 'rss add' → 自愈到 feed_manage。""" - reg = self._make_feed_registry() - names = [r["name"] for r in reg.search("rss add")] - assert "mcp_feed__feed_manage" in names, "query='rss add' 未找到 feed_manage" - - -# ── ToolSearchTool 执行测试 ─────────────────────────────────────────────────── - - -class TestToolSearchTool: - def test_returns_json_with_matched(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="定时任务")) - data = json.loads(result) - assert "matched" in data - assert any(r["name"] == "schedule" for r in data["matched"]) - - def test_no_match_returns_tip(self): - reg = ToolRegistry() - reg.register( - ToolSearchTool(reg), always_on=True, risk="read-only" - ) - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="xxxxxxxxxxxxxxx")) - data = json.loads(result) - assert data["matched"] == [] - assert "tip" in data - - def test_empty_query_returns_empty_not_all_tools(self): - """空/纯空白 query 不能返回全量工具目录(安全防护)。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - for bad_query in ["", " ", "\t\n"]: - result = asyncio.run(tool.execute(query=bad_query)) - data = json.loads(result) - assert data["matched"] == [], f"query={bad_query!r} 不应返回任何工具" - assert "tip" in data - - def test_empty_query_registry_search_returns_empty(self): - """registry.search 层面的空 query 保护。""" - reg = _make_registry() - assert reg.search("") == [] - assert reg.search(" ") == [] - - def test_top_k_respected(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="文件", top_k=2)) - data = json.loads(result) - assert len(data["matched"]) <= 2 - - def test_top_k_clamped_to_10(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="文件", top_k=999)) - data = json.loads(result) - assert len(data["matched"]) <= 10 - - # ── select: 精确加载路径 ───────────────────────────────────────────── - - def test_select_single_found(self): - """select:单个工具名 → 精确命中,返回完整结果。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:schedule")) - data = json.loads(result) - assert len(data["matched"]) == 1 - assert data["matched"][0]["name"] == "schedule" - assert data["matched"][0]["why_matched"] == ["名称:精确匹配"] - assert "tip" not in data - - def test_select_multi_found(self): - """select:A,B,C → 多个精确命中。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:schedule,write_file,memorize")) - data = json.loads(result) - names = [r["name"] for r in data["matched"]] - assert "schedule" in names - assert "write_file" in names - assert "memorize" in names - assert "tip" not in data - - def test_select_partial_match(self): - """select: 部分命中 → 返回 found 列表 + tip 说明 missing。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:schedule,nonexistent_tool")) - data = json.loads(result) - names = [r["name"] for r in data["matched"]] - assert "schedule" in names - assert "tip" in data - assert "nonexistent_tool" in data["tip"] - - def test_select_all_missing(self): - """select: 全部不存在 → matched 为空,tip 说明。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:ghost_tool,phantom_tool")) - data = json.loads(result) - assert data["matched"] == [] - assert "tip" in data - - def test_select_case_insensitive_prefix(self): - """SELECT: 大写前缀也能正常处理。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="SELECT:schedule")) - data = json.loads(result) - assert any(r["name"] == "schedule" for r in data["matched"]) - - def test_select_with_spaces(self): - """select: 工具名两侧有空格应被 strip。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select: schedule , write_file ")) - data = json.loads(result) - names = [r["name"] for r in data["matched"]] - assert "schedule" in names - assert "write_file" in names - - def test_select_deduplicates_requested_names(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - - data = json.loads( - asyncio.run(tool.execute(query="select:schedule,schedule,schedule")) - ) - - assert [item["name"] for item in data["matched"]] == ["schedule"] - assert data["unlocked"] == ["schedule"] - - def test_select_result_has_expected_fields(self): - """select: 结果包含 summary / risk / always_on 字段。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:schedule")) - data = json.loads(result) - r = data["matched"][0] - for field in ("name", "summary", "why_matched", "risk", "always_on"): - assert field in r, f"缺少字段: {field}" - - # ── excluded_names 排除:已可见工具不出现在搜索结果 ───────────────── - - def test_visible_tools_excluded_from_keyword_search(self): - """excluded_names 传入 schedule 时,keyword 搜索不应返回它。""" - reg = _make_registry() - results = reg.search("定时任务", excluded_names={"schedule"}) - assert all(r["name"] != "schedule" for r in results) - - def test_visible_tools_excluded_from_exact_fast_path(self): - """精确名称 fast path:工具名在 excluded_names 中时不返回。""" - reg = _make_registry() - results = reg.search("schedule", excluded_names={"schedule"}) - assert all(r["name"] != "schedule" for r in results) - - def test_no_excluded_names_searches_all(self): - """excluded_names 未传(None)时搜索全量工具,仅排除 meta 工具。""" - reg = _make_registry() - results = reg.search("定时任务") - assert any(r["name"] == "schedule" for r in results) - - def test_excluded_names_are_per_call_not_shared_state(self): - """excluded_names 是调用级参数,两次调用互不干扰(无共享 registry 状态)。""" - reg = _make_registry() - # Turn A:schedule 已可见 - results_a = reg.search("定时任务", excluded_names={"schedule"}) - # Turn B:schedule 未可见(另一 session 或下一轮) - results_b = reg.search("定时任务", excluded_names=set()) - assert all(r["name"] != "schedule" for r in results_a) - assert any(r["name"] == "schedule" for r in results_b) - - def test_get_deferred_names_excludes_visible(self): - """get_deferred_names(visible=...) 不包含已可见(preloaded)工具。""" - reg = _make_registry() - deferred = reg.get_deferred_names(visible={"schedule"}) - builtin = deferred["builtin"] - assert "schedule" not in builtin - # write_file 未在 visible 中,应出现在 deferred 里 - assert "write_file" in builtin - - def test_select_respects_allowed_risk(self): - """select: 加载时尊重 allowed_risk,write 工具在 read-only 过滤下不返回。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - # schedule 是 write 风险,只允许 read-only 时不应返回 - result = asyncio.run( - tool.execute(query="select:schedule", allowed_risk=["read-only"]) - ) - data = json.loads(result) - assert all(r["name"] != "schedule" for r in data.get("matched", [])) - assert "tip" in data - assert "风险等级不符" in data["tip"] - - def test_select_excludes_already_visible(self): - """select: 不返回已可见工具,tip 中说明可直接调用。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - - async def _run(): - return await tool.execute( - query="select:schedule", - excluded_names={"schedule"}, - ) - - data = json.loads(asyncio.run(_run())) - assert all(r["name"] != "schedule" for r in data.get("matched", [])) - assert "tip" in data - assert "schedule" in data["tip"] - assert data["already_loaded"] == ["schedule"] - - def test_select_risk_filter_uses_runtime_registry_view(self, monkeypatch): - base = ToolRegistry() - base_search = ToolSearchTool(base) - base.register(base_search, always_on=True, risk="read-only") - - runtime = base.fork() - runtime.register(_StubTool("runtime_write", "运行时写工具"), risk="write") - monkeypatch.setattr( - "agent.plugins.snapshot.get_current_runtime_snapshot", - lambda: SimpleNamespace(tool_registry=runtime), - ) - - data = json.loads( - asyncio.run( - base_search.execute( - query="select:runtime_write", - allowed_risk=["read-only"], - ) - ) - ) - - assert data["matched"] == [] - assert data["unlocked"] == [] - assert "风险等级不符" in data["tip"] - - def test_select_result_reports_unlocked_next_action(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - - data = json.loads(asyncio.run(tool.execute(query="select:schedule"))) - - assert data["unlocked"] == ["schedule"] - assert data["already_loaded"] == [] - assert "直接调用" in data["next_action"] - - def test_select_empty_result_keeps_unlock_shape(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - - data = json.loads(asyncio.run(tool.execute(query="select:"))) - - assert data["matched"] == [] - assert data["unlocked"] == [] - assert data["already_loaded"] == [] - - def test_keyword_result_reports_unlocked_next_action(self): - reg = _make_registry() - tool = ToolSearchTool(reg) - - data = json.loads(asyncio.run(tool.execute(query="定时任务", top_k=3))) - - assert any(name == "schedule" for name in data["unlocked"]) - assert data["already_loaded"] == [] - assert "再次 tool_search" in data["next_action"] - - def test_select_meta_tools_are_excluded(self): - """select:tool_search 与 search() 语义一致 → matched 为空。""" - reg = _make_registry() - tool = ToolSearchTool(reg) - result = asyncio.run(tool.execute(query="select:tool_search")) - data = json.loads(result) - assert data["matched"] == [], "select:tool_search 不应返回 meta tool" - assert "tip" in data - - # ── 精确名称 fast path(独立验证)─────────────────────────────────── - - def test_exact_name_fast_path(self): - """精确工具名查询命中 fast path,why_matched 为精确匹配。""" - reg = _make_registry() - results = reg.search("schedule") - assert results[0]["name"] == "schedule" - assert results[0]["why_matched"] == ["名称:精确匹配"] - - def test_exact_name_fast_path_respects_risk_filter(self): - """精确名称 fast path 仍然遵守 risk 过滤。""" - reg = _make_registry() - # schedule 是 write 风险,只允许 read-only 时不应返回 - results = reg.search("schedule", allowed_risk=["read-only"]) - assert all(r["name"] != "schedule" for r in results) - - -# ── Baseline 回归测试 ───────────────────────────────────────────────────────── - -_BASELINE_PATH = Path(__file__).parent / "fixtures" / "tool_search_baseline.json" - - -class TestBaseline: - """从 tool_search_baseline.json 加载固定 case,验证搜索质量不退化。 - - 每个 case 字段: - query 搜索词(必填) - expected_top1 top1 必须是该工具名(可选) - expected_top3 这些工具名必须全部出现在 top3 结果中(可选) - expected_excluded 这些工具名不能出现在结果中(可选) - allowed_risk 传给 search() 的 risk 过滤(可选) - """ - - @pytest.fixture(scope="class") - @classmethod - def reg(cls): - return _make_registry() - - @pytest.fixture(scope="class") - @classmethod - def cases(cls): - return json.loads(_BASELINE_PATH.read_text(encoding="utf-8")) - - def test_baseline_cases(self, reg, cases): - failures = [] - for case in cases: - query = case["query"] - allowed_risk = case.get("allowed_risk") - results = reg.search(query, top_k=5, allowed_risk=allowed_risk) - names = [r["name"] for r in results] - - expected_top1 = case.get("expected_top1") - if expected_top1 and (not names or names[0] != expected_top1): - failures.append( - f"query={query!r}: expected top1={expected_top1!r}, got {names[:3]}" - ) - - for want in case.get("expected_top3", []): - if want not in names[:3]: - failures.append( - f"query={query!r}: {want!r} not in top3, got {names[:3]}" - ) - - for excluded in case.get("expected_excluded", []): - if excluded in names: - failures.append( - f"query={query!r}: {excluded!r} should be excluded, got {names}" - ) - - if failures: - pytest.fail("\n".join(failures)) diff --git a/tests/test_turn_contract.py b/tests/test_turn_contract.py deleted file mode 100644 index 8105bb370..000000000 --- a/tests/test_turn_contract.py +++ /dev/null @@ -1,126 +0,0 @@ -from datetime import UTC, datetime, timedelta, timezone - -import pytest - -from agent.control.ids import new_item_id, new_thread_id, new_turn_id -from agent.control.models import ( - ThreadRecord, - ThreadSource, - TurnError, - TurnItem, - TurnItemKind, - TurnRecord, - TurnResult, - TurnStatus, - TurnUsage, -) - -NOW = datetime(2026, 7, 14, 8, 0, tzinfo=UTC) - - -def test_turn_status_values_and_terminal_membership() -> None: - assert [status.value for status in TurnStatus] == [ - "queued", - "in_progress", - "completed", - "interrupted", - "failed", - "cancelled", - ] - assert TurnStatus.COMPLETED.is_terminal - assert not TurnStatus.IN_PROGRESS.is_terminal - - -def test_ids_are_namespaced_and_unique() -> None: - generators = ( - (new_thread_id, "programmatic:"), - (new_turn_id, "turn:"), - (new_item_id, "item:"), - ) - for generate, prefix in generators: - values = {generate() for _ in range(100)} - assert len(values) == 100 - assert all(value.startswith(prefix) for value in values) - - -def test_thread_record_normalizes_utc_and_serializes_rfc3339() -> None: - offset = timezone(timedelta(hours=8)) - record = ThreadRecord( - id="telegram:1", - source=ThreadSource.CHANNEL, - created_at=datetime(2026, 7, 14, 16, tzinfo=offset), - updated_at=datetime(2026, 7, 14, 16, 1, tzinfo=offset), - ) - - assert record.created_at.tzinfo is UTC - assert record.to_dict()["createdAt"] == "2026-07-14T08:00:00Z" - - -def test_models_reject_naive_datetime() -> None: - with pytest.raises(ValueError, match="必须包含时区"): - ThreadRecord( - id="thread", - source=ThreadSource.INTERNAL, - created_at=datetime(2026, 7, 14), - updated_at=NOW, - ) - - -def test_turn_item_round_trip_keeps_discriminator() -> None: - item = TurnItem( - id="item:1", - kind=TurnItemKind.TOOL_CALL, - data={"name": "shell", "status": "completed"}, - ) - - assert TurnItem.from_dict(item.to_dict()) == item - assert item.to_dict()["type"] == "toolCall" - - -def test_turn_usage_rejects_fake_negative_counts() -> None: - with pytest.raises(ValueError, match="不得为负数"): - TurnUsage(input_tokens=-1) - - -def test_turn_result_has_stable_wire_shape_and_duration() -> None: - record = TurnRecord( - id="turn:1", - thread_id="programmatic:1", - status=TurnStatus.COMPLETED, - input="你好", - metadata={"source": "test"}, - items=[ - TurnItem( - id="item:1", - kind=TurnItemKind.ASSISTANT_MESSAGE, - data={"content": "你好"}, - ) - ], - usage=TurnUsage(input_tokens=10, output_tokens=2, coverage="exact"), - error=None, - created_at=NOW, - started_at=NOW + timedelta(seconds=1), - completed_at=NOW + timedelta(seconds=2, milliseconds=250), - final_response="你好", - ) - - payload = TurnResult.from_record(record).to_dict() - - assert payload["threadId"] == "programmatic:1" - assert payload["status"] == "completed" - assert payload["durationMs"] == 1250 - assert payload["usage"] == { - "inputTokens": 10, - "cachedInputTokens": None, - "outputTokens": 2, - "reasoningOutputTokens": None, - "requestCount": 0, - "coveredRequestCount": 0, - "coverage": "exact", - } - assert payload["error"] is None - - -def test_turn_error_requires_boolean_retryable_on_decode() -> None: - with pytest.raises(ValueError, match="必须是布尔值"): - TurnError.from_dict({"type": "provider", "message": "failed", "retryable": 1}) diff --git a/tests/test_turn_effects.py b/tests/test_turn_effects.py deleted file mode 100644 index bbc822062..000000000 --- a/tests/test_turn_effects.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -import pytest - -from agent.turn_effects import ( - PostCommitEffect, - post_commit_effect, - set_post_commit_effect, -) - - -def test_live_effect_ignores_legacy_memory_flag() -> None: - assert post_commit_effect({"skip_post_memory": True}) is PostCommitEffect.ALLOW - - -def test_structured_effect_rejects_invalid_shape_and_value() -> None: - with pytest.raises(ValueError, match="必须是 object"): - post_commit_effect({"effects": "suppress"}) - with pytest.raises(ValueError): - post_commit_effect({"effects": {"post_commit": "unknown"}}) - - -def test_effect_writer_preserves_sibling_effect_fields() -> None: - metadata: dict[str, object] = {"effects": {"audit": "allow"}} - - set_post_commit_effect(metadata, PostCommitEffect.SUPPRESS) - - assert metadata == {"effects": {"audit": "allow", "post_commit": "suppress"}} - - -def test_effect_writer_rejects_malformed_existing_container() -> None: - with pytest.raises(ValueError, match="必须是 object"): - set_post_commit_effect( - {"effects": "suppress"}, - PostCommitEffect.SUPPRESS, - ) diff --git a/tests/test_turn_pipelines.py b/tests/test_turn_pipelines.py deleted file mode 100644 index 03c48661f..000000000 --- a/tests/test_turn_pipelines.py +++ /dev/null @@ -1,755 +0,0 @@ -import asyncio -from datetime import UTC, datetime -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from agent.control.context import running_turn_id -from agent.core.passive_turn import ( - _collect_current_akashic_push_media, - _persistence_from_metadata, -) -from agent.core.runtime_support import SessionLike, TurnRunResult -from agent.looping.core import AgentLoop, _supports_stream_events -from agent.looping.interrupt import ActiveTurnState -from agent.lifecycle.facade import TurnLifecycle -from agent.lifecycle.types import BeforeReasoningCtx, BeforeTurnCtx -from agent.looping.ports import AgentLoopConfig, AgentLoopDeps -from agent.context import ContextBuilder -from agent.plugin_composition.channels import ( - ChannelDeliveryReceipt, - DeliveryStatus as ChannelDeliveryStatus, -) -from agent.looping.session_lane import SessionLaneRegistry -from agent.persona import reset_veda -from agent.plugin_composition import LLMResponse, ToolCall -from plugins.compaction.engine import ( - CommittedContextUnit, - ContextPayloadSegments, -) -from agent.tools.base import Tool -from agent.tools.registry import ToolRegistry -from agent.tools.web_fetch import WebFetchTool -from bus.event_bus import EventBus -from bus.events import InboundMessage, OutboundMessage -from bus.queue import MessageBus -from bus.events_lifecycle import TurnCommitted -from core.error_context import current_session_key -from bootstrap.wiring import wire_turn_lifecycle -from plugins.compaction.runtime import CompactionProjection -from session.store import CompactionHead -from tests.provider_fakes import ProviderContextBudgetStub -from tests.model_plugin_fakes import ( - BoundChatModelFake, - bind_test_model_snapshot, - build_test_chat_models, - build_test_model_store, -) -from tests.compaction_fakes import install_test_projection - - -class _NoopTool(Tool): - @property - def name(self) -> str: - return "noop" - - @property - def description(self) -> str: - return "noop" - - @property - def parameters(self) -> dict: - return {"type": "object", "properties": {}, "required": []} - - async def execute(self, **kwargs) -> str: - return "ok" - - -class _Provider(ProviderContextBudgetStub): - async def chat(self, **kwargs): - return LLMResponse(content="ok", tool_calls=[]) - - -class _TestOutboundPort: - async def dispatch(self, _outbound: object) -> ChannelDeliveryReceipt: - return ChannelDeliveryReceipt( - delivery_id="test-delivery", - status=ChannelDeliveryStatus.DELIVERED, - ) - - -def test_stream_events_support_realtime_private_channels(): - assert _supports_stream_events("telegram", "123") - assert not _supports_stream_events("telegram", "-1001") - assert not _supports_stream_events("telegram", "@alice") - assert _supports_stream_events("akashic", "shared-chat") - assert not _supports_stream_events("web", "retired-chat") - assert not _supports_stream_events("mobile", "retired-chat") - assert not _supports_stream_events("qq", "123") - assert not _supports_stream_events("cli", "direct") - - -def test_akashic_push_media_is_collected_only_for_the_current_session(): - media: list[str] = [] - _collect_current_akashic_push_media( - media, - { - "target_channel": "akashic", - "target_chat_id": "chat", - "image": "artifact:image", - }, - channel="akashic", - chat_id="chat", - ) - _collect_current_akashic_push_media( - media, - { - "target_channel": "telegram", - "target_chat_id": "chat", - "file": "artifact:file", - }, - channel="akashic", - chat_id="chat", - ) - - assert media == ["artifact:image"] - - -def test_stream_event_sink_respects_suppression_flag(): - loop = object.__new__(AgentLoop) - loop._event_bus = EventBus() - msg = InboundMessage( - channel="telegram", - sender="u", - chat_id="123", - content="hello", - metadata={"suppress_stream_events": True}, - ) - - assert AgentLoop._build_stream_event_sink(loop, msg) is None - - -@pytest.mark.asyncio -async def test_process_direct_accepts_generic_effect_metadata(): - loop = object.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = None - loop._process = AsyncMock( - return_value=OutboundMessage( - channel="telegram", - chat_id="123", - content="ok", - ) - ) - - result = await AgentLoop.process_direct_message( - loop, - content="天气", - session_key="scheduler:job", - channel="telegram", - chat_id="123", - metadata={ - "omit_user_turn": True, - "effects": {"post_commit": "suppress"}, - "disabled_prompt_sections": ["memory"], - }, - disabled_tools=["message_push"], - ) - - msg = loop._process.await_args.args[0] - assert result.content == "ok" - assert msg.metadata == { - "omit_user_turn": True, - "effects": {"post_commit": "suppress"}, - "disabled_prompt_sections": ["memory"], - "suppress_stream_events": True, - "disabled_tools": ["message_push"], - } - assert loop._process.await_args.kwargs["dispatch_outbound"] is False - - -@pytest.mark.asyncio -async def test_process_direct_in_memory_metadata_has_no_history_or_persistence(): - loop = object.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = None - loop._process = AsyncMock( - return_value=OutboundMessage( - channel="scheduler", - chat_id="job-1", - content="ok", - ) - ) - - await AgentLoop.process_direct_message( - loop, - content="天气", - session_key="scheduler:job-1", - channel="scheduler", - chat_id="job-1", - metadata={ - "omit_user_turn": True, - "omit_assistant_turn": True, - "skip_session_history": True, - "effects": {"post_commit": "suppress"}, - }, - ) - - msg = loop._process.await_args.args[0] - assert msg.metadata == { - "omit_user_turn": True, - "omit_assistant_turn": True, - "skip_session_history": True, - "effects": {"post_commit": "suppress"}, - "suppress_stream_events": True, - } - persistence = _persistence_from_metadata(msg.metadata) - assert persistence.persist_user is False - assert persistence.persist_assistant is False - - -@pytest.mark.asyncio -async def test_process_direct_runs_concurrently_with_another_session(): - loop = object.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = None - events: list[str] = [] - passive_started = asyncio.Event() - direct_started = asyncio.Event() - release_passive = asyncio.Event() - - async def _process( - msg: InboundMessage, - session_key: str | None = None, - busy_session_key: str | None = None, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - key = session_key or msg.session_key - events.append(f"start:{key}") - if key == "cli:1": - passive_started.set() - await release_passive.wait() - else: - direct_started.set() - events.append(f"end:{key}") - return OutboundMessage( - channel=msg.channel, - chat_id=msg.chat_id, - content=key, - ) - - loop._process = _process - passive_msg = InboundMessage( - channel="cli", - sender="u", - chat_id="1", - content="hello", - ) - passive_task = asyncio.create_task( - AgentLoop._process_with_runtime_admission(loop, passive_msg) - ) - await passive_started.wait() - direct_task = asyncio.create_task( - AgentLoop.process_direct( - loop, - content="天气", - session_key="scheduler:job", - channel="telegram", - chat_id="123", - ) - ) - await asyncio.wait_for(direct_started.wait(), timeout=1) - - assert events == ["start:cli:1", "start:scheduler:job", "end:scheduler:job"] - assert not passive_task.done() - release_passive.set() - - await asyncio.gather(passive_task, direct_task) - - assert events == [ - "start:cli:1", - "start:scheduler:job", - "end:scheduler:job", - "end:cli:1", - ] - assert loop._session_lanes._states == {} - - -@pytest.mark.asyncio -async def test_process_direct_waits_for_the_same_session_lane(): - loop = object.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = None - events: list[str] = [] - first_started = asyncio.Event() - release_first = asyncio.Event() - - async def _process( - msg: InboundMessage, - session_key: str | None = None, - busy_session_key: str | None = None, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - key = session_key or msg.session_key - events.append(f"start:{key}:{msg.content}") - if msg.content == "hello": - first_started.set() - await release_first.wait() - events.append(f"end:{key}:{msg.content}") - return OutboundMessage(msg.channel, msg.chat_id, key) - - loop._process = _process - passive_msg = InboundMessage("cli", "u", "1", "hello") - passive_task = asyncio.create_task( - AgentLoop._process_with_runtime_admission(loop, passive_msg) - ) - await first_started.wait() - direct_task = asyncio.create_task( - AgentLoop.process_direct( - loop, - content="second", - session_key="cli:1", - channel="cli", - chat_id="1", - ) - ) - - await asyncio.sleep(0.01) - assert events == ["start:cli:1:hello"] - assert not direct_task.done() - release_first.set() - await asyncio.gather(passive_task, direct_task) - - assert events == [ - "start:cli:1:hello", - "end:cli:1:hello", - "start:cli:1:second", - "end:cli:1:second", - ] - assert loop._session_lanes._states == {} - - -@pytest.mark.asyncio -async def test_process_direct_waits_for_explicit_busy_session_lane(): - loop = object.__new__(AgentLoop) - loop._session_lanes = SessionLaneRegistry() - loop._runtime_snapshot_store = None - events: list[str] = [] - first_started = asyncio.Event() - release_first = asyncio.Event() - - async def _process( - msg: InboundMessage, - session_key: str | None = None, - busy_session_key: str | None = None, - dispatch_outbound: bool = True, - ) -> OutboundMessage: - _ = busy_session_key, dispatch_outbound - key = session_key or msg.session_key - events.append(f"start:{key}") - if key == "cli:1": - first_started.set() - await release_first.wait() - events.append(f"end:{key}") - return OutboundMessage(msg.channel, msg.chat_id, key) - - loop._process = _process - passive_task = asyncio.create_task( - AgentLoop._process_with_runtime_admission( - loop, - InboundMessage("cli", "u", "1", "hello"), - ) - ) - await first_started.wait() - direct_task = asyncio.create_task( - AgentLoop.process_direct( - loop, - content="scheduled", - session_key="scheduler:job", - busy_session_key="cli:1", - channel="cli", - chat_id="1", - ) - ) - - await asyncio.sleep(0.01) - assert events == ["start:cli:1"] - assert not direct_task.done() - release_first.set() - await asyncio.gather(passive_task, direct_task) - - assert events == [ - "start:cli:1", - "end:cli:1", - "start:scheduler:job", - "end:scheduler:job", - ] - assert loop._session_lanes._states == {} - - -@pytest.mark.asyncio -async def test_cancelled_session_lane_waiter_does_not_block_reentry(): - lanes = SessionLaneRegistry() - first_entered = asyncio.Event() - release_first = asyncio.Event() - - async def hold_first() -> None: - async with lanes.hold("programmatic:one"): - first_entered.set() - await release_first.wait() - - async def wait_for_same_lane() -> None: - async with lanes.hold("programmatic:one"): - raise AssertionError("cancelled waiter entered the lane") - - first = asyncio.create_task(hold_first()) - await first_entered.wait() - waiter = asyncio.create_task(wait_for_same_lane()) - await asyncio.sleep(0) - waiter.cancel() - with pytest.raises(asyncio.CancelledError): - await waiter - - release_first.set() - await first - assert lanes._states == {} - async with lanes.hold("programmatic:one"): - assert list(lanes._states) == ["programmatic:one"] - assert lanes._states == {} - - -@pytest.mark.asyncio -async def test_process_uses_busy_session_key_for_processing_state(tmp_path: Path): - loop = _make_loop(tmp_path) - state = MagicMock() - loop._processing_state = state # type: ignore[attr-defined] - loop._react = AsyncMock( # type: ignore[method-assign] - return_value=OutboundMessage( - channel="telegram", - chat_id="123", - content="ok", - ) - ) - msg = InboundMessage( - channel="telegram", - sender="user", - chat_id="123", - content="天气", - ) - - async with bind_test_model_snapshot(_Provider()): - outbound = await loop._process( - msg, - session_key="scheduler:job", - busy_session_key="telegram:123", - dispatch_outbound=False, - ) - - assert outbound.content == "ok" - state.enter.assert_called_once_with("telegram:123") - state.exit.assert_called_once_with("telegram:123") - loop._react.assert_awaited_once() # type: ignore[attr-defined] - call = loop._react.await_args # type: ignore[attr-defined] - assert call.args == (msg, "scheduler:job") - assert call.kwargs["chat_models"] is not None - assert call.kwargs["model_id"] is None - assert call.kwargs["reasoning_effort"] is None - assert call.kwargs["dispatch_outbound"] is False - assert call.kwargs["command_admitted"] is True - - -@pytest.mark.asyncio -async def test_process_restores_session_context(tmp_path: Path): - loop = _make_loop(tmp_path) - loop._react = AsyncMock( # type: ignore[method-assign] - return_value=OutboundMessage( - channel="telegram", - chat_id="123", - content="ok", - ) - ) - msg = InboundMessage( - channel="telegram", - sender="user", - chat_id="123", - content="天气", - ) - token = current_session_key.set("outer-session") - try: - async with bind_test_model_snapshot(_Provider()): - await loop._process(msg, dispatch_outbound=False) - assert current_session_key.get() == "outer-session" - finally: - current_session_key.reset(token) - - -@pytest.mark.asyncio -async def test_process_restores_session_context_after_core_failure(tmp_path: Path): - loop = _make_loop(tmp_path) - state = MagicMock() - loop._processing_state = state # type: ignore[attr-defined] - loop._react = AsyncMock( # type: ignore[method-assign] - side_effect=RuntimeError("core failed") - ) - msg = InboundMessage( - channel="telegram", - sender="user", - chat_id="123", - content="天气", - ) - token = current_session_key.set("outer-session") - try: - with pytest.raises(RuntimeError, match="core failed"): - async with bind_test_model_snapshot(_Provider()): - await loop._process(msg, dispatch_outbound=False) - assert current_session_key.get() == "outer-session" - state.enter.assert_called_once_with("telegram:123") - state.exit.assert_called_once_with("telegram:123") - finally: - current_session_key.reset(token) - - -@pytest.mark.asyncio -async def test_process_does_not_run_removed_web_fetch_spill_cleanup( - tmp_path: Path, - caplog: pytest.LogCaptureFixture, -): - loop = _make_loop(tmp_path) - loop.tools.register(WebFetchTool(requester=cast(Any, object()))) - loop._react = AsyncMock( # type: ignore[method-assign] - side_effect=RuntimeError("provider failed") - ) - msg = InboundMessage( - channel="web", - sender="user", - chat_id="desktop-chat", - content="继续", - ) - - with pytest.raises(RuntimeError, match="provider failed"): - async with bind_test_model_snapshot(_Provider()): - await loop._process(msg, dispatch_outbound=False) - - assert "web_fetch_cleanup" not in caplog.text - - -def _make_loop(tmp_path: Path) -> AgentLoop: - _ = reset_veda(tmp_path) - tools = ToolRegistry() - tools.register(_NoopTool()) - loop = AgentLoop( - AgentLoopDeps( - bus=MessageBus(), - tools=tools, - session_manager=MagicMock(), - workspace=tmp_path, - context=ContextBuilder(tmp_path), - outbound_port=cast(Any, _TestOutboundPort()), - ), - AgentLoopConfig(), - ) - loop.session_manager.get_or_create.return_value = SimpleNamespace(metadata={}) - loop._runtime_snapshot_store = build_test_model_store(_Provider()) - return loop - - -@pytest.mark.asyncio -@pytest.mark.parametrize("abort_phase", ("before_turn", "before_reasoning")) -async def test_runtime_abort_never_enters_model_execution( - tmp_path: Path, - abort_phase: str, -) -> None: - loop = _make_loop(tmp_path) - session = MagicMock() - session.key = "cli:abort" - session.metadata = {} - session.messages = [] - session.get_history.return_value = [] - loop.session_manager.get_or_create.return_value = session - - class _RejectingChatModels: - calls = 0 - - def execution(self, **_selection: object) -> object: - self.calls += 1 - raise AssertionError("abort must not enter model execution") - - chat_models = _RejectingChatModels() - loop._runtime_snapshot_store = build_test_model_store( - _Provider(), - chat_models=chat_models, - ) - - async def abort(ctx: object) -> object: - ctx.abort = True # type: ignore[attr-defined] - ctx.abort_reply = f"{abort_phase} stopped" # type: ignore[attr-defined] - return ctx - - event_type = BeforeTurnCtx if abort_phase == "before_turn" else BeforeReasoningCtx - loop._event_bus.on(event_type, abort) - - result = await loop._process_with_runtime_admission( - InboundMessage("cli", "user", "abort", "hello"), - dispatch_outbound=False, - ) - - assert result.content == f"{abort_phase} stopped" - assert chat_models.calls == 0 - - -@pytest.mark.asyncio -async def test_runtime_admission_runs_normal_tool_loop_through_chat_models( - tmp_path: Path, -) -> None: - """真实准入链只从 CHAT_MODELS 取得一次 Turn-local 模型绑定。""" - - class _ToolProvider(ProviderContextBudgetStub): - def __init__(self) -> None: - self.responses = [ - LLMResponse( - content="", - tool_calls=[ToolCall("call-1", "noop", {})], - ), - LLMResponse(content="done", tool_calls=[]), - ] - self.calls = 0 - - async def chat(self, **_kwargs: object) -> LLMResponse: - self.calls += 1 - return self.responses.pop(0) - - loop = _make_loop(tmp_path) - session = MagicMock() - session.key = "cli:normal" - session.created_at = datetime(2026, 8, 29, tzinfo=UTC) - session.metadata = {} - session.messages = [] - session.get_history.return_value = [] - session.add_message.side_effect = lambda role, content, **kwargs: { - "role": role, - "content": content, - **kwargs, - } - loop.session_manager.get_or_create.return_value = session - loop.session_manager.append_messages = AsyncMock(return_value=None) - provider = _ToolProvider() - chat_models = build_test_chat_models(provider) - loop._runtime_snapshot_store = build_test_model_store( - provider, - chat_models=chat_models, - ) - - result = await loop._process_with_runtime_admission( - InboundMessage("cli", "user", "normal", "use noop"), - dispatch_outbound=False, - ) - - assert result.content == "done" - assert provider.calls == 2 - assert chat_models.execution_calls == 1 # type: ignore[attr-defined] - - -def test_agent_loop_fanouts_turn_committed_from_passive_turn(tmp_path: Path): - loop = _make_loop(tmp_path) - turn_events: list[TurnCommitted] = [] - loop._event_bus.on(TurnCommitted, lambda event: turn_events.append(event)) - session = MagicMock() - session.key = "cli:1" - session.messages = [] - session.metadata = {} - session.get_history = MagicMock(return_value=[]) - - def add_message(role: str, content: str, **kwargs: object) -> dict[str, object]: - message = {"role": role, "content": content, **kwargs} - session.messages.append(message) - return message - - session.add_message = MagicMock(side_effect=add_message) - loop.session_manager.get_or_create.return_value = session - loop.session_manager.append_messages = AsyncMock(return_value=None) - loop._reasoner.run_turn = AsyncMock( - return_value=TurnRunResult( - reply="ok", - tool_chain=[ - { - "text": "", - "calls": [ - { - "name": "noop", - "arguments": {"x": 1}, - "result": "done", - } - ], - } - ], - context_retry={ - "react_stats": { - "iteration_count": 1, - "turn_input_sum_tokens": 100, - } - }, - ) - ) - - msg = InboundMessage(channel="cli", sender="u", chat_id="1", content="hello") - - async def _process_and_drain() -> None: - provider = _Provider() - await loop._react( - msg, - msg.session_key, - chat_models=build_test_chat_models(provider), - ) - await loop._event_bus.drain() - await loop._event_bus.aclose() - - asyncio.run(_process_and_drain()) - - assert turn_events - turn_event = turn_events[0] - assert turn_event.session_key == "cli:1" - assert turn_event.persisted_user_message == "hello" - assert turn_event.assistant_response == "ok" - assert turn_event.tool_chain_raw[0]["calls"][0]["name"] == "noop" - assert turn_event.react_stats["iteration_count"] == 1 - assert turn_event.react_stats["turn_input_sum_tokens"] == 100 - - -@pytest.mark.asyncio -async def test_agent_loop_afterstep_fires_with_turn_lifecycle_wiring(tmp_path: Path): - loop = _make_loop(tmp_path) - session_key = "cli:123" - loop._active_turn_states[session_key] = ActiveTurnState(session_key=session_key) - wire_turn_lifecycle( - lifecycle=TurnLifecycle(loop._event_bus), - active_turn_states=loop.active_turn_states, - ) - msg = InboundMessage(channel="cli", sender="u", chat_id="123", content="你好") - session = SimpleNamespace( - key=session_key, - created_at=datetime(2026, 1, 1, tzinfo=UTC), - messages=[], - metadata={}, - last_consolidated=0, - get_history=MagicMock(return_value=[]), - history_units=MagicMock(return_value=[]), - add_message=MagicMock(), - ) - loop.session_manager.get_or_create.return_value = session - - provider = _Provider() - await loop._reasoner.run_turn( - msg=msg, - session=cast(SessionLike, session), - agent_model=BoundChatModelFake(provider), - fallback_model=BoundChatModelFake(provider), - base_history=[], - ) - - state = loop._active_turn_states[session_key] - assert state.partial_reply == "ok" - assert state.tools_used == [] - assert state.tool_chain_partial == [] diff --git a/tests/test_veda.py b/tests/test_veda.py deleted file mode 100644 index 3a8063d4f..000000000 --- a/tests/test_veda.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import annotations - -import stat -from pathlib import Path - -import pytest - -from agent.persona import ( - VedaLoadError, - read_default_veda, - read_veda, - reset_veda, - veda_path, -) - - -def test_read_veda_returns_nonempty_utf8(tmp_path: Path) -> None: - path = veda_path(tmp_path) - path.parent.mkdir(parents=True) - path.write_text("\ncustom veda\n", encoding="utf-8") - - assert read_veda(tmp_path) == "custom veda" - - -@pytest.mark.parametrize( - ("payload", "message"), - [ - (None, "缺少 Veda"), - (b" \n", "Veda 内容为空"), - (b"\xff", "Veda 不是合法 UTF-8"), - ], -) -def test_read_veda_fails_loud_without_fallback( - tmp_path: Path, - payload: bytes | None, - message: str, -) -> None: - if payload is not None: - path = veda_path(tmp_path) - path.parent.mkdir(parents=True) - path.write_bytes(payload) - - with pytest.raises(VedaLoadError, match=message): - read_veda(tmp_path) - - -def test_reset_veda_backs_up_original_bytes_and_restores_default( - tmp_path: Path, -) -> None: - path = veda_path(tmp_path) - path.parent.mkdir(parents=True) - original = b"\xffbroken" - path.write_bytes(original) - - result = reset_veda(tmp_path) - - assert result.changed is True - assert result.backup_path is not None - assert result.backup_path.read_bytes() == original - assert stat.S_IMODE(result.backup_path.stat().st_mode) == 0o600 - assert stat.S_IMODE(result.backup_path.parent.stat().st_mode) == 0o700 - assert stat.S_IMODE(result.backup_path.parent.parent.stat().st_mode) == 0o700 - assert read_veda(tmp_path) == read_default_veda() - assert result.previous_sha256 is not None - - -def test_reset_veda_is_idempotent_for_default_content(tmp_path: Path) -> None: - first = reset_veda(tmp_path) - second = reset_veda(tmp_path) - - assert first.changed is True - assert first.backup_path is None - assert second.changed is False - assert second.backup_path is None - assert not (tmp_path / "memory/veda-backups").exists() diff --git a/tests/test_verify_host_runtime_deployment.py b/tests/test_verify_host_runtime_deployment.py deleted file mode 100644 index a31a08e6a..000000000 --- a/tests/test_verify_host_runtime_deployment.py +++ /dev/null @@ -1,114 +0,0 @@ -from __future__ import annotations - -import json -import subprocess -from pathlib import Path - -import pytest - -from scripts.verify_host_runtime_deployment import ( - verify_deployment_image, - verify_host_toolchain_deployment, -) - - -def test_deployment_requires_exact_engine_image( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - image = "sha256:" + "a" * 64 - manifest = tmp_path / "release.json" - manifest.write_text( - json.dumps({"schemaVersion": 1, "imageId": image}), encoding="utf-8" - ) - - def run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]: - return subprocess.CompletedProcess("docker", 0, image + "\n", "") - - monkeypatch.setattr(subprocess, "run", run) - assert verify_deployment_image(manifest, image) == image - - -def test_deployment_rejects_mutable_tag(tmp_path: Path) -> None: - manifest = tmp_path / "release.json" - manifest.write_text("{}", encoding="utf-8") - with pytest.raises(RuntimeError, match="content-addressed"): - verify_deployment_image(manifest, "akashic:latest") - - -def test_deployment_rejects_runtime_env_toolchain_digest( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - checkout = tmp_path / "checkout" - checkout.mkdir() - identity = { - "schemaVersion": 1, - "releaseCommit": "a" * 40, - "miseConfigSha256": "b" * 64, - "tools": {"python": "3.14.6"}, - "toolchainDigest": "c" * 64, - } - manifest = tmp_path / "release.json" - manifest.write_text( - json.dumps({"hostToolchainIdentity": identity}), encoding="utf-8" - ) - monkeypatch.setattr( - "scripts.verify_host_runtime_deployment.resolve_toolchain_identity", - lambda _checkout, _mise: identity, - ) - - with pytest.raises(RuntimeError, match="toolchain"): - verify_host_toolchain_deployment( - manifest, - checkout, - tmp_path / "mise", - tmp_path / "python", - "d" * 64, - ) - - -def test_toolchain_verification_preserves_venv_python_symlink( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path -) -> None: - checkout = tmp_path / "checkout" - module = checkout / "agent" / "host_bridge" / "server.py" - module.parent.mkdir(parents=True) - module.write_text("", encoding="utf-8") - target = tmp_path / "python-target" - target.write_text("", encoding="utf-8") - target.chmod(0o755) - launcher = tmp_path / "venv" / "bin" / "python" - launcher.parent.mkdir(parents=True) - launcher.symlink_to(target) - identity = { - "schemaVersion": 1, - "releaseCommit": "a" * 40, - "miseConfigSha256": "b" * 64, - "tools": {"python": "3.14.6"}, - "toolchainDigest": "c" * 64, - } - manifest = tmp_path / "release.json" - manifest.write_text( - json.dumps({"hostToolchainIdentity": identity}), encoding="utf-8" - ) - monkeypatch.setattr( - "scripts.verify_host_runtime_deployment.resolve_toolchain_identity", - lambda _checkout, _mise: identity, - ) - calls: list[list[str]] = [] - - def run( - arguments: list[str], **_kwargs: object - ) -> subprocess.CompletedProcess[str]: - calls.append(arguments) - output = "Python 3.14.6\n" if arguments[1] == "--version" else f"{module}\n" - return subprocess.CompletedProcess(arguments, 0, output, "") - - monkeypatch.setattr(subprocess, "run", run) - assert ( - verify_host_toolchain_deployment( - manifest, checkout, tmp_path / "mise", launcher, "c" * 64 - ) - == identity - ) - assert calls[0][0] == str(launcher.absolute()) - assert calls[1][0] == str(launcher.absolute()) diff --git a/tests/test_vision_tool.py b/tests/test_vision_tool.py deleted file mode 100644 index 217f60d76..000000000 --- a/tests/test_vision_tool.py +++ /dev/null @@ -1,269 +0,0 @@ -from __future__ import annotations - -from contextlib import asynccontextmanager -from pathlib import Path -import asyncio -import base64 -import json - -import pytest - -from agent.plugin_composition import ( - BoundModelDescriptor, - CapabilitySources, - LLMResponse, - ModelCapabilities, - ModelRequest, - ModelRole, -) -from agent.plugin_composition.models import ModelUnavailableError -from agent.tools.vision import ReadImageVisionTool -from plugins.computer.mcp_server import screenshot_result -from tests.model_plugin_fakes import bind_test_model_snapshot - - -class _VisionModel: - def __init__(self, error: Exception | None = None) -> None: - self.error = error - self.requests: list[ModelRequest] = [] - - async def complete(self, request: ModelRequest) -> LLMResponse: - self.requests.append(request) - if self.error is not None: - raise self.error - return LLMResponse(content="一只猫") - - -class _BoundModel: - def __init__( - self, - responder: _VisionModel, - *, - role: ModelRole, - revision: int, - ) -> None: - self.responder = responder - self._descriptor = BoundModelDescriptor( - binding_id=f"vision:{revision}:{role.value}", - plugin_snapshot_id="test-plugin-snapshot", - model_revision=revision, - model_id=f"{role.value}-{revision}", - connection_id="vision-connection", - driver_id="vision-driver", - driver_contract_version="1", - auth_identity="vision-test", - model=f"{role.value}-{revision}", - role=role, - reasoning_effort=None, - capabilities=ModelCapabilities(input_modalities=("text", "image")), - capability_sources=CapabilitySources(input_modalities="test"), - capability_digest=f"capabilities-{revision}", - ) - - @property - def descriptor(self) -> BoundModelDescriptor: - return self._descriptor - - async def complete(self, request: ModelRequest) -> LLMResponse: - assert isinstance(request, ModelRequest) - assert not hasattr(request, "model") - assert not hasattr(request, "provider") - return await self.responder.complete(request) - - -class _ChatModels: - def __init__(self, model: _VisionModel) -> None: - self.model = model - self.roles: list[ModelRole] = [] - self.execution_calls = 0 - self.execution_exits = 0 - self.revision = 1 - self.current: object | None = None - self.owner: asyncio.Task[object] | None = None - self.executions: list[object] = [] - - @asynccontextmanager - async def execution(self, **_selection: object): - if self.current is not None: - if asyncio.current_task() is not self.owner: - raise RuntimeError("model execution 不能由子 task 继承") - yield self.current - return - self.execution_calls += 1 - facade = self - revision = self.revision - - class _Execution: - def __init__(self) -> None: - self.models = { - role: _BoundModel(facade.model, role=role, revision=revision) - for role in (ModelRole.AGENT, ModelRole.VISION) - } - - def chat(self, role: ModelRole) -> _BoundModel: - facade.roles.append(role) - return self.models[role] - - execution = _Execution() - self.current = execution - self.owner = asyncio.current_task() - self.executions.append(execution) - try: - yield execution - finally: - self.current = None - self.owner = None - self.execution_exits += 1 - - -@pytest.mark.asyncio -async def test_vision_tool_uses_turn_vision_binding( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - image = tmp_path / "image.png" - image.write_bytes(b"fixture") - monkeypatch.setattr( - "agent.tools.vision.encode_image_data_uri", - lambda _path: "data:image/png;base64,AA==", - ) - model = _VisionModel() - chat_models = _ChatModels(model) - - async with bind_test_model_snapshot(object(), chat_models=chat_models): - async with chat_models.execution() as execution: - agent = execution.chat(ModelRole.AGENT) - chat_models.revision = 2 - result = await ReadImageVisionTool().execute(str(image), "图里有什么?") - vision = execution.chat(ModelRole.VISION) - - assert result == "一只猫" - assert chat_models.execution_calls == 1 - assert chat_models.execution_exits == 1 - assert chat_models.roles == [ - ModelRole.AGENT, - ModelRole.VISION, - ModelRole.VISION, - ] - assert agent.descriptor.plugin_snapshot_id == vision.descriptor.plugin_snapshot_id - assert agent.descriptor.model_revision == vision.descriptor.model_revision == 1 - assert len(model.requests) == 1 - request = model.requests[0] - assert request.max_output_tokens == 2048 - assert request.disable_reasoning is True - content = request.messages[0]["content"] - assert content[0] == {"type": "text", "text": "图里有什么?"} - assert content[1]["image_url"]["url"] == "data:image/png;base64,AA==" - - async with bind_test_model_snapshot(object(), chat_models=chat_models): - await ReadImageVisionTool().execute(str(image), "下一轮") - assert chat_models.execution_calls == 2 - latest = chat_models.executions[-1].models[ModelRole.VISION] # type: ignore[attr-defined] - assert latest.descriptor.model_revision == 2 - - -@pytest.mark.asyncio -async def test_computer_screenshot_path_is_readable_by_vision_tool( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - data_dir = tmp_path / "computer-data" - monkeypatch.setenv("AKA_PLUGIN_DATA_DIR", str(data_dir)) - screenshot = base64.b64decode( - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=" - ) - response = screenshot_result(screenshot, "image/png") - content = response["content"] - assert isinstance(content, list) - content_block = content[0] - assert isinstance(content_block, dict) - text = content_block.get("text") - assert isinstance(text, str) - reference = json.loads(text) - path = reference["path"] - assert isinstance(path, str) - model = _VisionModel() - chat_models = _ChatModels(model) - - async with bind_test_model_snapshot(object(), chat_models=chat_models): - result = await ReadImageVisionTool(allowed_dir=data_dir).execute( - path, "描述 Computer 当前画面" - ) - - assert result == "一只猫" - content = model.requests[0].messages[0]["content"] - assert content[1]["image_url"]["url"].startswith("data:image/png;base64,") - - -@pytest.mark.asyncio -async def test_vision_tool_preserves_public_model_error( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - image = tmp_path / "image.png" - image.write_bytes(b"fixture") - monkeypatch.setattr( - "agent.tools.vision.encode_image_data_uri", - lambda _path: "data:image/png;base64,AA==", - ) - chat_models = _ChatModels(_VisionModel(ModelUnavailableError("vision missing"))) - - async with bind_test_model_snapshot(object(), chat_models=chat_models): - result = await ReadImageVisionTool().execute(str(image), "describe") - - assert result == "调用视觉模型失败:vision missing" - - -@pytest.mark.asyncio -async def test_vision_tool_does_not_hide_internal_image_errors( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - image = tmp_path / "image.png" - image.write_bytes(b"fixture") - - def fail(_path: Path) -> str: - raise AssertionError("internal-marker") - - monkeypatch.setattr("agent.tools.vision.encode_image_data_uri", fail) - - with pytest.raises(AssertionError, match="internal-marker"): - await ReadImageVisionTool().execute(str(image), "describe") - - -@pytest.mark.asyncio -async def test_vision_tool_rejects_execution_without_turn_snapshot( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - image = tmp_path / "image.png" - image.write_bytes(b"fixture") - monkeypatch.setattr( - "agent.tools.vision.encode_image_data_uri", - lambda _path: "data:image/png;base64,AA==", - ) - - with pytest.raises(RuntimeError, match="exact Turn snapshot"): - await ReadImageVisionTool().execute(str(image), "describe") - - -@pytest.mark.asyncio -async def test_vision_tool_rejects_inherited_child_task( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - image = tmp_path / "image.png" - image.write_bytes(b"fixture") - monkeypatch.setattr( - "agent.tools.vision.encode_image_data_uri", - lambda _path: "data:image/png;base64,AA==", - ) - chat_models = _ChatModels(_VisionModel()) - - async with bind_test_model_snapshot(object(), chat_models=chat_models): - async with chat_models.execution(): - child = asyncio.create_task( - ReadImageVisionTool().execute(str(image), "child") - ) - with pytest.raises(RuntimeError, match="exact Turn snapshot"): - await child diff --git a/tests/test_wake_drift_fixture.py b/tests/test_wake_drift_fixture.py deleted file mode 100644 index c01b08e0f..000000000 --- a/tests/test_wake_drift_fixture.py +++ /dev/null @@ -1,82 +0,0 @@ -import asyncio -import shutil -from datetime import UTC, datetime -from pathlib import Path - -import pytest - -from agent.plugins.manager import PluginManager -from bus.event_bus import EventBus -from plugins.eventmail.store import EventMailStore -from plugins.drift.store import DriftStore - - -async def _eventually(predicate) -> None: - for _ in range(200): - if predicate(): - return - await asyncio.sleep(0.01) - raise AssertionError("condition did not settle") - - -@pytest.mark.asyncio -async def test_fixture_submits_through_two_narrow_services_only_on_formal_start( - tmp_path: Path, -) -> None: - root = Path(__file__).resolve().parents[1] - plugin_root = tmp_path / "plugins" - content_dir = plugin_root / "content" - drift_dir = plugin_root / "drift" - fixture_dir = plugin_root / "wake_drift_gate" - shutil.copytree(root / "plugins" / "eventmail", content_dir) - shutil.copytree(root / "plugins" / "drift", drift_dir) - shutil.copytree(root / "tests" / "fixtures" / "wake_drift_gate", fixture_dir) - workspace = tmp_path / "workspace" - manager = PluginManager( - plugin_dirs=[content_dir, drift_dir, fixture_dir], - event_bus=EventBus(), - workspace=workspace, - installed_cache_root=tmp_path / "cache", - ) - await manager.load_all() - content = EventMailStore( - workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3" - ) - drift = DriftStore( - workspace / "plugin-data" / "drift-builtin" / "drift.sqlite3" - ) - assert content.state_counts() == {} - assert drift.snapshot(datetime.now(UTC))["proposals"] == () - - lifecycle = asyncio.create_task(manager.run_runtime_services()) - try: - await _eventually(lambda: content.state_counts() == {"pending": 1}) - assert len(drift.snapshot(datetime.now(UTC))["proposals"]) == 1 - - with (fixture_dir / "plugin.py").open("a", encoding="utf-8") as handle: - handle.write("\n# candidate fixture revision\n") - candidate = await manager.prepare_candidate("wake_drift_gate") - assert candidate is not None - assert content.state_counts() == {"pending": 1} - assert len(drift.snapshot(datetime.now(UTC))["proposals"]) == 1 - await manager.discard_prepared("wake_drift_gate") - finally: - lifecycle.cancel() - _ = await asyncio.gather(lifecycle, return_exceptions=True) - await manager.terminate_all() - - -def test_fixture_declares_structural_services_without_importing_domain_plugins() -> None: - source = ( - Path(__file__).resolve().parent - / "fixtures" - / "wake_drift_gate" - / "plugin.py" - ).read_text(encoding="utf-8") - - assert "from plugins.eventmail" not in source - assert "from plugins.drift" not in source - assert 'ServiceKey[ContentSourceServices]("eventmail.content_source.v1")' in source - assert 'ServiceKey[DriftProposalServices]("drift.proposals.v1")' in source - assert "SCOPED_TURNS" not in source - assert "TIMERS" not in source diff --git a/tests/test_wake_state.py b/tests/test_wake_state.py deleted file mode 100644 index b144c5dd5..000000000 --- a/tests/test_wake_state.py +++ /dev/null @@ -1,518 +0,0 @@ -import math -import sqlite3 -from collections.abc import Mapping -from contextlib import closing -from datetime import UTC, datetime, timedelta -from pathlib import Path - -import pytest -from fastapi import FastAPI -from fastapi.testclient import TestClient - -from agent.plugin_composition import DashboardContext -from plugins.wake.dashboard import register as register_dashboard -from plugins.wake.pool import build_initial_score -from plugins.wake.state import ContentScore, WakeState - - -def _item(sequence: int, score: float) -> dict[str, object]: - now = "2026-08-23T09:00:00+00:00" - return { - "ref": { - "source_id": "feed", - "item_id": f"item:{sequence}", - "revision": "1", - "state_version": 1, - }, - "payload": { - "preprocess_score": score, - "published_at": now, - }, - "snapshot_seq": sequence, - "status": "pending", - "observed_at": now, - "not_before": now, - "due": True, - } - - -def _scored( - state: WakeState, - items: tuple[dict[str, object], ...] | list[dict[str, object]], - now: datetime, -) -> tuple[Mapping[str, object], ...]: - due = state.unscored_due_items(items) - state.record_content_scores( - tuple( - ContentScore( - source_id=str(item["ref"]["source_id"]), # type: ignore[index] - item_id=str(item["ref"]["item_id"]), # type: ignore[index] - revision=str(item["ref"]["revision"]), # type: ignore[index] - initial_score=build_initial_score( - float(item["payload"]["preprocess_score"]), # type: ignore[index] - has_published_at=True, - wake_eligible=True, - ), - semantic_interest=0.0, - scored_at=now, - ) - for item in due - ) - ) - return state.scored_items(items) - - -def test_low_value_batch_advances_watermark_without_starting_turn(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - items = [_item(index, 0.001) for index in range(1, 21)] - - result = state.evaluate(_scored(state, items, now), snapshot_seq=20, now=now) - - assert result.should_wake is False - assert state.has_unseen_due(items, now) is False - assert state.unseen_deadline(items) is None - - -def test_new_content_rechecks_pool_without_a_hidden_refractory( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - first = [_item(1, 0.9)] - first_result = state.evaluate( - _scored(state, first, now), snapshot_seq=1, now=now - ) - assert first_result.should_wake - assert first_result.pool_mass == pytest.approx(-math.log1p(-0.9)) - assert state.has_unseen_due(first, now) is True - state.commit_content_admission(first) - - second = [*first, _item(2, 0.05)] - result = state.evaluate(_scored(state, second, now), snapshot_seq=2, now=now) - - assert result.should_wake is True - assert state.has_unseen_due(second, now) is True - - -def test_future_item_remains_unseen_after_current_batch_is_evaluated(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - tomorrow = now + timedelta(days=1) - state = WakeState(tmp_path / "wake.sqlite3") - current = _item(1, 0.5) - future = _item(2, 0.9) - future["not_before"] = tomorrow.isoformat() - future["due"] = False - - first = state.evaluate( - _scored(state, [current, future], now), snapshot_seq=2, now=now - ) - - assert first.should_wake is False - assert state.unseen_deadline((current, future)) == tomorrow - future["due"] = True - second = state.evaluate( - _scored(state, [current, future], tomorrow), - snapshot_seq=2, - now=tomorrow, - ) - assert second.should_wake is True - assert second.driver_item_id == "item:2" - - -def test_pool_expiry_requires_low_mass_and_minimum_residence(tmp_path) -> None: - now = datetime(2026, 8, 25, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - old_low = _item(1, 0.001) - old_low["observed_at"] = (now - timedelta(hours=25)).isoformat() - old_high = _item(2, 0.9) - old_high["observed_at"] = (now - timedelta(hours=25)).isoformat() - young_low = _item(3, 0.001) - young_low["observed_at"] = (now - timedelta(hours=23)).isoformat() - - items = _scored(state, [old_low, old_high, young_low], now) - expired = state.expired_content_refs( - items, - now=now, - minimum_residence=timedelta(hours=24), - ) - - assert [ref["item_id"] for ref in expired] == ["item:1"] - - -def test_maintenance_deadline_uses_last_durable_fire(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - - assert state.next_maintenance_deadline( - now, interval=timedelta(minutes=5) - ) == now + timedelta(minutes=5) - state.begin_attempt( - attempt_id="attempt:heartbeat", - timer_id="timer:heartbeat", - scheduled_for=now, - fired_at=now, - ) - - assert state.next_maintenance_deadline( - now, interval=timedelta(minutes=5) - ) == now + timedelta(minutes=5) - - -def test_pool_audit_measures_seen_content_from_its_stored_score(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - item = _item(1, 0.9) - scored = _scored(state, [item], now) - state.commit_content_admission(scored) - - audit = state.audit_pool(scored, now=now + timedelta(hours=1)) - - assert audit.should_wake is False - assert audit.new_mass == 0.0 - assert audit.pool_mass > 0.0 - - -@pytest.mark.parametrize( - ("source_id", "revision"), - (("other-feed", "1"), ("feed", "2")), -) -def test_new_mass_uses_full_content_identity( - tmp_path, source_id: str, revision: str -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - old_high = _item(1, 0.7) - old_ref = old_high["ref"] - assert isinstance(old_ref, dict) - old_ref["item_id"] = "shared-id" - old_support = _item(3, 0.7) - old_support["ref"]["source_id"] = "support-feed" # type: ignore[index] - old_scored = _scored(state, [old_high, old_support], now) - state.commit_content_admission(old_scored) - - new_low = _item(2, 0.001) - new_ref = new_low["ref"] - assert isinstance(new_ref, dict) - new_ref.update( - {"source_id": source_id, "item_id": "shared-id", "revision": revision} - ) - items = _scored( - state, [old_high, old_support, new_low], now + timedelta(hours=12) - ) - result = state.evaluate( - items, - snapshot_seq=2, - now=now + timedelta(hours=12), - ) - - assert result.should_wake is True - assert result.new_mass == 0.0 - assert state.has_unseen_due(items, now) is True - - -def test_seen_content_stays_in_pool_and_amplifies_next_new_kick(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - old_high = _item(1, 0.999) - new_low = _item(2, 0.05) - old_high["ref"]["source_id"] = "old-feed" # type: ignore[index] - pooled = WakeState(tmp_path / "pooled.sqlite3") - old_scored = _scored(pooled, [old_high], now) - pooled.commit_content_admission(old_scored) - - pooled_items = _scored(pooled, [old_high, new_low], now + timedelta(hours=1)) - pooled_result = pooled.evaluate( - pooled_items, - snapshot_seq=2, - now=now + timedelta(hours=1), - ) - isolated_old = _item(1, 0.001) - isolated_old["ref"]["source_id"] = "old-feed" # type: ignore[index] - isolated = WakeState(tmp_path / "isolated.sqlite3") - isolated_scored = _scored(isolated, [isolated_old], now) - isolated.commit_content_admission(isolated_scored) - isolated_items = _scored( - isolated, [isolated_old, new_low], now + timedelta(hours=1) - ) - isolated_result = isolated.evaluate( - isolated_items, - snapshot_seq=2, - now=now + timedelta(hours=1), - ) - - assert pooled_result.new_mass == isolated_result.new_mass - assert pooled_result.pool_mass > isolated_result.pool_mass - assert pooled_result.should_wake is True - assert isolated_result.should_wake is False - - -def test_pool_sums_fixed_scores_and_uses_one_threshold(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - items = [_item(1, 0.61), _item(2, 0.4)] - - result = state.evaluate(_scored(state, items, now), snapshot_seq=2, now=now) - - assert result.should_wake is True - assert result.pool_mass == pytest.approx( - build_initial_score(0.61, has_published_at=True, wake_eligible=True) - + build_initial_score(0.4, has_published_at=True, wake_eligible=True) - ) - assert result.pool_mass > result.threshold - assert result.threshold == 1.0 - - -def test_fixed_score_decays_monotonically_then_stops_contributing(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - item = _item(1, 0.5) - scored = _scored(state, [item], now) - - fresh = state.audit_pool(scored, now=now) - older = state.audit_pool(scored, now=now + timedelta(hours=36)) - stale = state.audit_pool(scored, now=now + timedelta(days=8)) - - assert fresh.pool_mass > older.pool_mass > 0.0 - assert stale.pool_mass == 0.0 - assert stale.below_floor == 1 - - -def test_content_initial_score_is_immutable_per_revision(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - score = ContentScore("feed", "item:1", "1", 0.4, 0.2, now) - state.record_content_scores((score,)) - state.record_content_scores((score,)) - - with pytest.raises(RuntimeError, match="初始分 identity 冲突"): - state.record_content_scores( - (ContentScore("feed", "item:1", "1", 0.5, 0.2, now),) - ) - - -def test_old_schema_requires_installation_eventmail_migration(tmp_path) -> None: - path = tmp_path / "wake.sqlite3" - state = WakeState(path) - state.initialize() - with closing(sqlite3.connect(path)) as connection, connection: - connection.execute("PRAGMA user_version = 1") - - with pytest.raises(RuntimeError, match="EventMail 安装迁移"): - WakeState(path).initialize() - - -def test_new_schema_contains_no_alert_or_context_source_tables(tmp_path) -> None: - path = tmp_path / "wake.sqlite3" - state = WakeState(path) - state.initialize() - - with closing(sqlite3.connect(path)) as connection: - assert connection.execute("PRAGMA user_version").fetchone() == (8,) - tables = { - str(row[0]) - for row in connection.execute( - "SELECT name FROM sqlite_master " - "WHERE type='table' AND name NOT LIKE 'sqlite_%'" - ) - } - assert tables == { - "admission_state", - "seen_content", - "content_scores", - "wake_runs", - "wake_attempts", - } - - -def test_same_version_schema_mutation_fails_loud(tmp_path) -> None: - path = tmp_path / "wake.sqlite3" - state = WakeState(path) - state.initialize() - with closing(sqlite3.connect(path)) as connection, connection: - connection.execute("ALTER TABLE seen_content RENAME TO old_seen_content") - connection.execute("CREATE TABLE seen_content(item_identity TEXT)") - - with pytest.raises(RuntimeError, match="schema mismatch"): - WakeState(path).initialize() - - -def test_dashboard_run_projection_records_one_decision(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - state.record_screen( - run_id="run_fixture", - owner="content", - candidates_seen=4, - screening=({"candidate_id": "candidate_1", "question": "New?"},), - started_at=now, - ) - state.record_decision( - run_id="run_fixture", - decision="skip", - detail="No new capability", - completed_at=now, - ) - assert state.get_run("run_fixture")["decision"] == "skip" # type: ignore[index] - - -def test_timer_attempt_records_no_due_check(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - state.begin_attempt( - attempt_id="attempt:one", - timer_id="timer:one", - scheduled_for=now, - fired_at=now, - ) - state.set_attempt_mail_watermark(attempt_id="attempt:one", mail_watermark=7) - state.finish_attempt( - attempt_id="attempt:one", - outcome="no_due", - owner=None, - detail="定时检查完成,没有可处理信件", - completed_at=now, - ) - - assert state.count_attempts() == 1 - attempt = state.get_attempt("attempt:one") - assert attempt is not None - assert attempt["outcome"] == "no_due" - assert attempt["mail_watermark"] == 7 - - -@pytest.mark.parametrize( - "outcome", - ( - "content_insufficient", - "admission_rejected", - "shared", - "model_skip", - "deferred", - "cancelled_after_fire", - "delivery_unknown", - "failed", - ), -) -def test_timer_attempt_accepts_each_terminal_outcome(tmp_path, outcome: str) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / f"{outcome}.sqlite3") - state.begin_attempt( - attempt_id=f"attempt:{outcome}", - timer_id="timer:one", - scheduled_for=now, - fired_at=now, - ) - state.set_attempt_mail_watermark(attempt_id=f"attempt:{outcome}", mail_watermark=3) - - state.finish_attempt( - attempt_id=f"attempt:{outcome}", - outcome=outcome, - owner="content", - detail=outcome, - completed_at=now, - ) - - assert state.get_attempt(f"attempt:{outcome}")["outcome"] == outcome # type: ignore[index] - - -def test_dashboard_lists_no_due_timer_attempt(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - data_root = tmp_path / "plugin-data/wake-builtin" - state = WakeState(data_root / "wake.sqlite3") - state.begin_attempt( - attempt_id="attempt:dashboard", - timer_id="timer:dashboard", - scheduled_for=now, - fired_at=now, - ) - state.set_attempt_mail_watermark(attempt_id="attempt:dashboard", mail_watermark=4) - state.finish_attempt( - attempt_id="attempt:dashboard", - outcome="no_due", - owner=None, - detail="No due EventMail", - completed_at=now, - ) - app = FastAPI() - register_dashboard( - app, - DashboardContext( - plugin_id="wake", - plugin_dir=tmp_path / "plugins/wake", - data_root=data_root, - validation=False, - ), - ) - - response = TestClient(app).get("/api/dashboard/wake/attempts") - - assert response.status_code == 200 - assert response.json()["items"][0]["outcome"] == "no_due" - assert response.json()["total"] == 1 - - -def test_dashboard_shows_attempt_closed_by_restart(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - data_root = tmp_path / "plugin-data/wake-builtin" - state = WakeState(data_root / "wake.sqlite3") - state.begin_attempt( - attempt_id="attempt:restart", - timer_id="timer:restart", - scheduled_for=now, - fired_at=now, - ) - state.set_attempt_mail_watermark(attempt_id="attempt:restart", mail_watermark=8) - assert state.close_interrupted_attempts(now + timedelta(seconds=2)) == 1 - assert state.close_interrupted_attempts(now + timedelta(seconds=3)) == 0 - app = FastAPI() - register_dashboard( - app, - DashboardContext( - plugin_id="wake", - plugin_dir=tmp_path / "plugins/wake", - data_root=data_root, - validation=False, - ), - ) - - response = TestClient(app).get("/api/dashboard/wake/attempts/attempt%3Arestart") - - assert response.status_code == 200 - assert response.json()["outcome"] == "delivery_unknown" - - -def test_dashboard_exposes_fired_then_closed_attempt_without_watermark( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - data_root = tmp_path / "plugin-data/wake-builtin" - state = WakeState(data_root / "wake.sqlite3") - state.begin_attempt( - attempt_id="attempt:closed", - timer_id="timer:closed", - scheduled_for=now, - fired_at=now, - ) - state.finish_attempt( - attempt_id="attempt:closed", - outcome="cancelled_after_fire", - owner=None, - detail="Timer fired before close", - completed_at=now, - ) - app = FastAPI() - register_dashboard( - app, - DashboardContext( - plugin_id="wake", - plugin_dir=tmp_path / "plugins/wake", - data_root=data_root, - validation=False, - ), - ) - - response = TestClient(app).get("/api/dashboard/wake/attempts/attempt%3Aclosed") - - assert response.status_code == 200 - assert response.json()["outcome"] == "cancelled_after_fire" - assert response.json()["mail_watermark"] is None diff --git a/tests/test_wake_v3.py b/tests/test_wake_v3.py deleted file mode 100644 index 6109a4b35..000000000 --- a/tests/test_wake_v3.py +++ /dev/null @@ -1,2053 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -import math -from collections.abc import Mapping -from datetime import UTC, datetime, timedelta -from types import SimpleNamespace -from typing import Any, cast - -import pytest - -from agent.control.models import TurnError, TurnItem, TurnItemKind, TurnStatus -from agent.control.scoped_turn import DurableTurnView, TurnAcceptedReceipt -from agent.control.timer import TimerReceipt, TimerStatus -from agent.lifecycle.types import BeforeTurnCtx -from agent.turn_effects import PostCommitEffect, TurnStorage -from agent.plugin_composition import PluginScopedTurns, PluginTimers -from agent.plugin_composition.durable_deliveries import PluginDurableDeliveries -from agent.plugin_composition.durable_delivery_store import DurableDeliveryStore -from plugins.wake.plugin import EVENTMAIL_ALERT_DELIVERY -from plugins.wake.plugin import ( - ContentWakeServices, - DeliveryTarget, - DriftWakeServices, - WakeRuntime, - _ScreenedItem, - _candidate_id, -) -from plugins.wake.state import WakeState - -_CONTENT_REF = { - "source_id": "fixture", - "item_id": "item:1", - "revision": "1", - "state_version": 1, -} -_CONTENT_CANDIDATE = _candidate_id(_CONTENT_REF) - - -def _content_receipt() -> dict[str, object]: - return { - "selection_token": "content:selection", - "items": ({"ref": dict(_CONTENT_REF), "payload": {}},), - } - - -def _decision_item(name: str, arguments: dict[str, object]) -> TurnItem: - return TurnItem( - TurnItemKind.TOOL_CALL, - f"item:{name}", - { - "callId": f"call:{name}", - "name": name, - "status": "success", - "arguments": arguments, - "resultPreview": '{"recorded":true}', - }, - ) - - -class _TimerHandle: - def __init__(self, deadline: datetime) -> None: - self.deadline = deadline - self.future: asyncio.Future[TimerReceipt] = ( - asyncio.get_running_loop().create_future() - ) - - @property - def id(self) -> str: - return "timer:wake" - - async def result(self) -> TimerReceipt: - return await asyncio.shield(self.future) - - async def cancel(self) -> TimerReceipt: - if not self.future.done(): - self.future.set_result(self._receipt(TimerStatus.CANCELLED)) - return await self.future - - async def cleanup(self) -> None: - _ = await self.cancel() - - def fire(self) -> None: - self.future.set_result(self._receipt(TimerStatus.FIRED)) - - def _receipt(self, status: TimerStatus) -> TimerReceipt: - return TimerReceipt(self.id, self.deadline, self.deadline, status) - - -class _Timers: - def __init__(self) -> None: - self.handles: list[_TimerHandle] = [] - - def schedule(self, deadline: datetime) -> _TimerHandle: - handle = _TimerHandle(deadline) - self.handles.append(handle) - return handle - - -class _TurnHandle: - def __init__( - self, - status: TurnStatus = TurnStatus.COMPLETED, - *, - turn_id: str = "turn:1", - items: list[TurnItem] | None = None, - ) -> None: - self.accepted = TurnAcceptedReceipt("wake:default", turn_id) - self._result = SimpleNamespace( - id=turn_id, - thread_id="wake:default", - status=status, - final_response="hello" if status is TurnStatus.COMPLETED else None, - error=None, - items=items - or [ - _decision_item( - "share_content", - {"message": "hello", "items": [_CONTENT_CANDIDATE]}, - ) - ], - ) - - async def result(self): - return self._result - - async def cleanup(self) -> None: - return None - - -class _Turns: - def __init__(self) -> None: - self.starts: list[dict[str, object]] = [] - self.reads: dict[TurnAcceptedReceipt, DurableTurnView] = {} - self.started = asyncio.Event() - - async def ensure_session(self, key: str, *, metadata) -> str: - assert metadata == {"programmatic": True, "wake": True} - return key - - async def start(self, session_id: str, content: str, **kwargs): - self.starts.append({"session_id": session_id, "content": content, **kwargs}) - self.started.set() - scope = kwargs["scope"] - turn_id = f"turn:{len(self.starts)}" - if scope.terminal_tools == ("screen_content",): - return _TurnHandle( - turn_id=turn_id, - items=[ - _decision_item( - "screen_content", - { - "items": [ - { - "candidate_id": _CONTENT_CANDIDATE, - "initial_interest": "likely_interesting", - "question": "值得进一步确认吗?", - } - ] - }, - ) - ], - ) - return _TurnHandle(turn_id=turn_id) - - def read(self, accepted: TurnAcceptedReceipt) -> DurableTurnView: - if accepted not in self.reads: - raise KeyError(accepted) - return self.reads[accepted] - - -class _BlockingTurnHandle: - def __init__(self, release: asyncio.Event) -> None: - self.accepted = TurnAcceptedReceipt("wake:default", "turn:blocking") - self._release = release - - async def result(self): - await self._release.wait() - return SimpleNamespace( - id="turn:blocking", - thread_id="wake:default", - status=TurnStatus.FAILED, - final_response=None, - error=None, - items=[], - ) - - async def cleanup(self) -> None: - return None - - -class _BlockingTurns(_Turns): - def __init__(self) -> None: - super().__init__() - self.release = asyncio.Event() - - async def start(self, session_id: str, content: str, **kwargs): - self.starts.append({"session_id": session_id, "content": content, **kwargs}) - self.started.set() - return _BlockingTurnHandle(self.release) - - -class _Content: - def __init__(self, now: datetime, payload: dict[str, object] | None = None) -> None: - self.now = now - self.payload = payload - self.cas_wins = True - self.snapshots = 0 - self.selects = 0 - self.expired_refs: set[tuple[str, str, str]] = set() - self.transitions: list[tuple[str, str, datetime | None]] = [] - self.selected_rows: list[dict[str, object]] = [] - self.alerts: list[dict[str, object]] = [] - self.closed_alerts: dict[tuple[str, str], str] = {} - self.contexts: list[dict[str, object]] = [] - - def snapshot(self, now: datetime): - self.snapshots += 1 - items = () - if self.payload is not None: - item = { - "ref": dict(_CONTENT_REF), - "payload": self.payload, - "snapshot_seq": 1, - "status": "pending", - "observed_at": self.now.isoformat(), - "not_before": self.now.isoformat(), - "due": now >= self.now, - } - ref = cast(dict[str, object], item["ref"]) - identity = ( - str(ref["source_id"]), - str(ref["item_id"]), - str(ref["revision"]), - ) - if identity not in self.expired_refs: - items = (item,) - return { - "snapshot_seq": 1, - "earliest_not_before": self.now.isoformat() if items else None, - "items": items, - } - - def expire(self, item_refs, now): - expired = [] - for item_ref in item_refs: - identity = ( - str(item_ref["source_id"]), - str(item_ref["item_id"]), - str(item_ref["revision"]), - ) - if identity in self.expired_refs: - continue - self.expired_refs.add(identity) - expired.append(dict(item_ref)) - return {"expired": tuple(expired), "stale": ()} - - def select(self, item_ref, snapshot_seq, accepted_turn, now): - return self.select_batch((item_ref,), snapshot_seq, accepted_turn, now) - - def select_batch(self, item_refs, snapshot_seq, accepted_turn, now): - self.selects += 1 - if not self.cas_wins: - return {"selected": False, "selection_token": None} - row = { - "selection_token": "content:selection", - "status": "selected", - "accepted_turn": dict(accepted_turn), - "payload": self.payload or {}, - "items": tuple( - {"ref": dict(item_ref), "payload": self.payload or {}} - for item_ref in item_refs - ), - } - self.selected_rows = [row] - return {"selected": True, "selection_token": "content:selection"} - - def selection(self, accepted_turn): - return next( - ( - row - for row in self.selected_rows - if row["accepted_turn"] == dict(accepted_turn) - ), - None, - ) - - def selected(self, limit: int = 100): - return tuple(self.selected_rows[:limit]) - - def transition(self, token, action, *, not_before=None, selected_refs=None): - self.transitions.append((token, action, not_before)) - self.selected_rows = [ - row for row in self.selected_rows if row["selection_token"] != token - ] - return {"changed": True, "status": action} - - def mail_watermark(self): - return 1 if self.payload is not None else 0 - - def report_alert( - self, *, source_id, event_id, payload, observed_at, expires_at=None - ): - self.alerts = [ - { - "source_id": source_id, - "event_id": event_id, - "payload": dict(payload), - "observed_at": observed_at.isoformat(), - "not_before": observed_at.isoformat(), - "expires_at": None if expires_at is None else expires_at.isoformat(), - "accepted_turn": None, - } - ] - - def report_context( - self, *, source_id, event_id, payload, observed_at, expires_at=None - ): - self.contexts = [ - { - "source_id": source_id, - "event_id": event_id, - "payload": dict(payload), - "observed_at": observed_at.isoformat(), - "expires_at": None if expires_at is None else expires_at.isoformat(), - } - ] - - def alert_deadline(self, now): - for alert in tuple(self.alerts): - expires_at = alert["expires_at"] - if ( - expires_at is not None - and datetime.fromisoformat(str(expires_at)) <= now - ): - self.expire_alert(alert["source_id"], alert["event_id"], now) - due = [ - datetime.fromisoformat(str(alert["not_before"])) - for alert in self.alerts - if alert["accepted_turn"] is None - and ( - alert["expires_at"] is None - or datetime.fromisoformat(str(alert["expires_at"])) > now - ) - ] - return min(due) if due else None - - def select_alert(self, accepted_turn, now): - if self.alert_deadline(now) is None: - return None - self.alerts[0]["accepted_turn"] = dict(accepted_turn) - return dict(self.alerts[0]) - - def selected_alert(self, accepted_turn): - return next( - ( - dict(alert) - for alert in self.alerts - if alert["accepted_turn"] == dict(accepted_turn) - ), - None, - ) - - def selected_alerts(self): - return tuple(dict(alert) for alert in self.alerts if alert["accepted_turn"]) - - def expire_alert(self, source_id, event_id, now): - before = len(self.alerts) - self.alerts = [ - alert - for alert in self.alerts - if not ( - alert["source_id"] == source_id - and alert["event_id"] == event_id - and alert["expires_at"] is not None - and datetime.fromisoformat(str(alert["expires_at"])) <= now - ) - ] - changed = len(self.alerts) != before - if changed: - self.closed_alerts[(source_id, event_id)] = "expired" - return changed - - def defer_alert(self, source_id, event_id, not_before): - self.alerts[0]["not_before"] = not_before.isoformat() - self.alerts[0]["accepted_turn"] = None - - def close_alert(self, source_id, event_id, status): - self.closed_alerts[(source_id, event_id)] = status - self.alerts = [ - alert - for alert in self.alerts - if not (alert["source_id"] == source_id and alert["event_id"] == event_id) - ] - - def alert_status(self, source_id, event_id): - if (source_id, event_id) in self.closed_alerts: - return self.closed_alerts[(source_id, event_id)] - return next( - ( - "selected" if alert["accepted_turn"] else "pending" - for alert in self.alerts - if alert["source_id"] == source_id and alert["event_id"] == event_id - ), - None, - ) - - def active_context(self, now): - return tuple( - context - for context in self.contexts - if context["expires_at"] is None - or datetime.fromisoformat(str(context["expires_at"])) > now - ) - - -class _BatchContent(_Content): - count = 20 - - def snapshot(self, now: datetime): - self.snapshots += 1 - items = tuple( - { - "ref": { - "source_id": "fixture", - "item_id": f"item:{index}", - "revision": "1", - "state_version": 1, - }, - "payload": { - "title": f"Title {index}", - "preprocess_score": 1 - index / 100, - "published_at": self.now.isoformat(), - }, - "snapshot_seq": index + 1, - "status": "pending", - "observed_at": self.now.isoformat(), - "not_before": self.now.isoformat(), - "due": now >= self.now, - } - for index in range(self.count) - if ("fixture", f"item:{index}", "1") not in self.expired_refs - ) - return { - "snapshot_seq": self.count, - "earliest_not_before": self.now.isoformat(), - "items": items, - } - - -class _DeferredContent(_Content): - def snapshot(self, now: datetime): - snapshot = super().snapshot(now) - snapshot["items"] = tuple( - {**dict(item), "status": "deferred"} - for item in cast(tuple[dict[str, object], ...], snapshot["items"]) - ) - return snapshot - - -class _Drift: - def __init__(self, now: datetime, payload: dict[str, object] | None = None) -> None: - self.now = now - self.payload = payload - self.cas_wins = True - self.snapshots = 0 - self.selects = 0 - self.transitions: list[tuple[str, str]] = [] - self.selected_rows: list[dict[str, object]] = [] - - def snapshot(self, now: datetime): - self.snapshots += 1 - proposals = () - if self.payload is not None: - proposals = ( - { - "ref": { - "proposal_id": "reflection", - "revision": "1", - "state_version": 1, - }, - "payload": self.payload, - "due": now >= self.now, - "next_due": (self.now + timedelta(minutes=5)).isoformat(), - }, - ) - return { - "next_due": self.now.isoformat() if proposals else None, - "proposals": proposals, - } - - def select(self, ref, accepted_turn, now): - self.selects += 1 - if not self.cas_wins: - return {"selected": False, "selection_token": None} - row = { - "selection_token": "drift:selection", - "status": "selected", - "accepted_turn": dict(accepted_turn), - "payload": self.payload or {}, - } - self.selected_rows = [row] - return {"selected": True, "selection_token": "drift:selection"} - - def transition(self, token, action): - self.transitions.append((token, action)) - self.selected_rows = [ - row for row in self.selected_rows if row["selection_token"] != token - ] - return {"changed": True, "status": action} - - def selected(self, limit: int = 100): - return tuple(self.selected_rows[:limit]) - - def selection(self, accepted_turn): - return next( - ( - row - for row in self.selected_rows - if row["accepted_turn"] == dict(accepted_turn) - ), - None, - ) - - -def _runtime( - now: datetime, - content: _Content, - drift: _Drift, - *, - state: WakeState | None = None, -): - timers = _Timers() - turns = _Turns() - runtime = WakeRuntime( - cast(PluginTimers, timers), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, content), - cast(DriftWakeServices, drift), - state=state, - now=lambda: now, - ) - return runtime, timers, turns - - -def _ctx(now: datetime, *, channel: str = "wake") -> BeforeTurnCtx: - return BeforeTurnCtx( - session_key="wake:default", - channel=channel, - chat_id="wake:default", - content="check", - timestamp=now, - history_messages=(), - turn_id="turn:1", - ) - - -@pytest.mark.asyncio -async def test_content_wins_without_reading_or_writing_drift() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"kind": "fitbit", "preprocess_score": 0.9}) - drift = _Drift(now, {"prompt": "reflect"}) - runtime, _timers, _turns = _runtime(now, content, drift) - ctx = _ctx(now) - - await runtime.prepare(ctx) - - assert ctx.abort is False - assert "【Wake Content 初筛】" in ctx.extra_hints[0] - assert '"kind":"fitbit"' in ctx.extra_hints[0] - assert content.selects == 0 - assert drift.snapshots == drift.selects == 0 - - -@pytest.mark.asyncio -async def test_content_screen_receives_one_frozen_twenty_candidate_page() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _BatchContent(now, {"kind": "fixture"}) - runtime, _timers, _turns = _runtime(now, content, _Drift(now)) - ctx = _ctx(now) - - await runtime.prepare(ctx) - - assert content.selects == 0 - candidates = json.loads(ctx.extra_hints[0].split("候选:\n", 1)[1]) - assert len(candidates) == 20 - assert all( - candidate["candidate_id"].startswith("candidate_") for candidate in candidates - ) - - -@pytest.mark.asyncio -async def test_successful_selection_consumes_kick_from_full_snapshot(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - - class LargeBatchContent(_BatchContent): - count = 101 - - content = LargeBatchContent(now, {"kind": "fixture"}) - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - ) - - admission = await runtime._admit_attempt() - assert admission.turn_owner == "content" - runtime._active_owner = "content" - await runtime.prepare(_ctx(now)) - proposal = runtime._content_proposal - assert proposal is not None - first_ref = cast(dict[str, object], proposal[1].candidates[0]["ref"]) - runtime._screened_content = ( - _ScreenedItem(_candidate_id(first_ref), "likely", "Confirm?"), - ) - runtime._phase = "content_investigate" - second = _ctx(now) - second.turn_id = "turn:2" - await runtime.prepare(second) - - assert state.has_unseen_due(content.snapshot(now)["items"], now) is False - - -@pytest.mark.asyncio -async def test_context_events_enter_only_content_investigation(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - content = _Content( - now, - { - "title": "Model update", - "preprocess_score": 0.9, - "published_at": now.isoformat(), - }, - ) - content.report_context( - source_id="steam", - event_id="current", - payload={"presence": "in_game"}, - observed_at=now, - expires_at=now + timedelta(minutes=10), - ) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - proactive_context="Do not interrupt sleep.", - ) - admission = await runtime._admit_attempt() - assert admission.turn_owner == "content" - runtime._active_owner = "content" - first = _ctx(now) - await runtime.prepare(first) - - assert "PROACTIVE_CONTEXT.md" in first.extra_hints[0] - assert "ContextEvent" not in first.extra_hints[0] - - runtime._screened_content = ( - _ScreenedItem(_CONTENT_CANDIDATE, "likely", "Is this substantial?"), - ) - runtime._phase = "content_investigate" - second = _ctx(now) - second.turn_id = "turn:2" - await runtime.prepare(second) - - prompt = second.extra_hints[0] - assert content.selects == 1 - assert "你总共有 20 轮调查预算" in prompt - assert '"presence":"in_game"' in prompt - assert "Is this substantial?" in prompt - - -@pytest.mark.asyncio -async def test_alert_bypasses_interest_and_receives_context(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - content = _Content(now, {"preprocess_score": 0.001}) - content.report_alert( - source_id="calendar", - event_id="meeting:1", - payload={"title": "Meeting in ten minutes"}, - observed_at=now, - ) - content.report_context( - source_id="steam", - event_id="current", - payload={"presence": "in_game"}, - observed_at=now, - expires_at=now + timedelta(minutes=30), - ) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - proactive_context="Do not interrupt sleep.", - ) - - admission = await runtime._admit_attempt() - assert admission.turn_owner == "alert" - assert content.snapshots == 1 - runtime._phase = "alert" - ctx = _ctx(now) - await runtime.prepare(ctx) - - assert "【Wake Alert】" in ctx.extra_hints[0] - assert "Do not interrupt sleep." in ctx.extra_hints[0] - assert '"presence":"in_game"' in ctx.extra_hints[0] - view = DurableTurnView( - "wake:default", - "turn:1", - TurnStatus.FAILED, - None, - "fixture", - "invalid alert", - False, - (), - ) - await runtime._settle_alert(TurnAcceptedReceipt("wake:default", "turn:1"), view) - assert content.alert_status("calendar", "meeting:1") == "skipped" - assert state.list_runs()[0]["decision"] == "skip" - - -@pytest.mark.asyncio -async def test_due_alert_does_not_block_content_pool_expiry(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now - timedelta(hours=25), {"preprocess_score": 0.001}) - content.report_alert( - source_id="calendar", - event_id="meeting:pool-maintenance", - payload={"title": "Meeting now"}, - observed_at=now, - ) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(tmp_path / "wake.sqlite3"), - now=lambda: now, - ) - - admission = await runtime._admit_attempt() - - assert admission.turn_owner == "alert" - assert content.expired_refs == {("fixture", "item:1", "1")} - assert "active=0" in admission.detail - assert "expired=1" in admission.detail - assert "threshold=1.000000" in admission.detail - - -@pytest.mark.asyncio -async def test_pool_maintenance_records_while_scoped_turn_is_running(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, None) - content.report_alert( - source_id="calendar", - event_id="meeting:long-turn", - payload={"title": "Long running alert"}, - observed_at=now, - ) - timers = _Timers() - turns = _BlockingTurns() - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - assert len(timers.handles) == 2 - duty_handle = min(timers.handles, key=lambda handle: handle.deadline) - maintenance_handle = max(timers.handles, key=lambda handle: handle.deadline) - duty_handle.fire() - await asyncio.wait_for(turns.started.wait(), timeout=1) - assert turns.release.is_set() is False - - maintenance_handle.fire() - for _ in range(20): - await asyncio.sleep(0) - terminal = [ - attempt - for attempt in state.list_attempts() - if attempt["outcome"] != "checking" - ] - if terminal: - break - - assert len(turns.starts) == 1 - assert terminal[0]["outcome"] == "no_due" - assert "maintenance_only=1" in str(terminal[0]["detail"]) - assert "threshold=1.000000" in str(terminal[0]["detail"]) - turns.release.set() - for _ in range(20): - await asyncio.sleep(0) - if not any( - attempt["outcome"] == "checking" for attempt in state.list_attempts() - ): - break - await runtime.close() - - -@pytest.mark.asyncio -async def test_expired_alert_is_not_admitted_after_restart(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - path = tmp_path / "wake.sqlite3" - state = WakeState(path) - content = _Content(now) - content.report_alert( - source_id="calendar", - event_id="old-meeting", - payload={"title": "Old meeting"}, - observed_at=now - timedelta(hours=1), - expires_at=now - timedelta(minutes=1), - ) - recovered = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(path), - now=lambda: now, - ) - - admission = await recovered._admit_attempt() - assert admission.turn_owner is None - assert content.alert_status("calendar", "old-meeting") == "expired" - - -@pytest.mark.asyncio -async def test_content_declines_then_drift_wins() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"wake_action": "decline"}) - drift = _Drift(now, {"prompt": "reflect"}) - runtime, _timers, _turns = _runtime(now, content, drift) - ctx = _ctx(now) - - await runtime.prepare(ctx) - - assert content.transitions == [("content:selection", "await_change", None)] - assert drift.selects == 1 - assert '"owner":"drift"' in ctx.extra_hints[0] - - -@pytest.mark.asyncio -async def test_both_decline_commit_transitions_then_quiet_abort() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"wake_action": "decline"}) - drift = _Drift(now, {"wake_action": "decline"}) - runtime, _timers, _turns = _runtime(now, content, drift) - ctx = _ctx(now) - - await runtime.prepare(ctx) - - assert ctx.abort is True and ctx.abort_reply == "" - assert content.transitions[0][1] == "await_change" - assert drift.transitions == [("drift:selection", "defer")] - - -@pytest.mark.asyncio -async def test_content_cas_lost_is_quiet_and_never_falls_through_to_drift() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"kind": "calendar"}) - content.cas_wins = False - drift = _Drift(now, {"prompt": "reflect"}) - runtime, _timers, _turns = _runtime(now, content, drift) - ctx = _ctx(now) - - await runtime.prepare(ctx) - runtime._screened_content = ( - _ScreenedItem(_CONTENT_CANDIDATE, "likely", "confirm"), - ) - runtime._phase = "content_investigate" - second = _ctx(now) - second.turn_id = "turn:2" - await runtime.prepare(second) - - assert second.abort is True and drift.snapshots == drift.selects == 0 - - -@pytest.mark.asyncio -async def test_non_wake_channel_has_zero_domain_reads_or_writes() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"kind": "feed"}) - drift = _Drift(now, {"prompt": "reflect"}) - runtime, _timers, _turns = _runtime(now, content, drift) - - await runtime.prepare(_ctx(now, channel="scheduler")) - - assert content.snapshots == content.selects == 0 - assert drift.snapshots == drift.selects == 0 - - -@pytest.mark.asyncio -async def test_timer_no_due_rechecks_without_starting_turn(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - future = now + timedelta(hours=1) - content = _Content(future, {"kind": "future"}) - drift = _Drift(future, None) - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, turns := _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, drift), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - assert len(timers.handles) == 2 - timers.handles[0].fire() - for _ in range(10): - await asyncio.sleep(0) - attempts = state.list_attempts() - if attempts and attempts[0]["outcome"] == "no_due": - break - assert turns.starts == [] - attempts = state.list_attempts() - assert len(attempts) == 1 - assert attempts[0]["outcome"] == "no_due" - assert attempts[0]["owner"] is None - assert "new_mass=0.000000" in str(attempts[0]["detail"]) - assert "pool_mass=0.000000" in str(attempts[0]["detail"]) - assert "threshold=1.000000" in str(attempts[0]["detail"]) - await runtime.close() - - -@pytest.mark.asyncio -async def test_start_rejects_runtime_without_durable_state() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, _Content(now)), - cast(DriftWakeServices, _Drift(now)), - now=lambda: now, - ) - - with pytest.raises(RuntimeError, match="缺少 durable state"): - await runtime.start() - - -@pytest.mark.asyncio -async def test_mail_watermark_fault_still_records_failed_timer_attempt( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - future = now + timedelta(hours=1) - - class BrokenWatermarkContent(_Content): - def mail_watermark(self): - raise RuntimeError("watermark unavailable") - - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, BrokenWatermarkContent(future, {"kind": "future"})), - cast(DriftWakeServices, _Drift(future, None)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - timers.handles[0].fire() - for _ in range(10): - await asyncio.sleep(0) - attempts = state.list_attempts() - if attempts and attempts[0]["outcome"] == "failed": - break - - attempts = state.list_attempts() - assert len(attempts) == 1 - assert attempts[0]["outcome"] == "failed" - assert attempts[0]["mail_watermark"] is None - assert attempts[0]["detail"] == "RuntimeError: watermark unavailable" - await runtime.close() - - -@pytest.mark.asyncio -async def test_maintenance_fault_records_failure_and_rearms(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - - class OneFaultContent(_Content): - watermark_calls = 0 - - def mail_watermark(self): - self.watermark_calls += 1 - if self.watermark_calls == 1: - raise RuntimeError("one maintenance fault") - return 0 - - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, OneFaultContent(now, None)), - cast(DriftWakeServices, _Drift(now, None)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - assert len(timers.handles) == 1 - first = timers.handles[0] - first.fire() - for _ in range(20): - await asyncio.sleep(0) - if len(timers.handles) == 2: - break - - first_attempt = state.list_attempts()[0] - assert first_attempt["outcome"] == "failed" - assert first_attempt["detail"] == "RuntimeError: one maintenance fault" - second = timers.handles[1] - assert second.deadline == first.deadline + timedelta(minutes=5) - - second.fire() - for _ in range(20): - await asyncio.sleep(0) - attempts = state.list_attempts() - if len(attempts) == 2 and attempts[0]["outcome"] != "checking": - break - - assert attempts[0]["outcome"] == "no_due" - assert "maintenance_only=1" in str(attempts[0]["detail"]) - await runtime.close() - - -@pytest.mark.asyncio -async def test_deferred_content_is_maintained_without_starting_a_turn( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - turns = _BlockingTurns() - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, turns), - cast( - ContentWakeServices, - _DeferredContent(now, {"preprocess_score": 0.9}), - ), - cast(DriftWakeServices, _Drift(now, None)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - assert len(timers.handles) == 1 - timers.handles[0].fire() - for _ in range(20): - await asyncio.sleep(0) - attempts = state.list_attempts() - if attempts and attempts[0]["outcome"] != "checking": - break - - assert turns.started.is_set() is False - assert attempts[0]["outcome"] == "content_insufficient" - detail = str(attempts[0]["detail"]) - assert "maintenance_only=1" in detail - assert "deferred_retry" not in detail - assert all( - field in detail - for field in ( - "active=", - "due=", - "expired=", - "scored=", - "new=", - "new_mass=", - "pool_mass=", - "threshold=", - "below_floor=", - "driver=", - ) - ) - await runtime.close() - - -@pytest.mark.asyncio -async def test_fired_timer_closed_before_duty_check_records_terminal_attempt( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - future = now + timedelta(hours=1) - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, _Content(future, {"kind": "future"})), - cast(DriftWakeServices, _Drift(future, None)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - timers.handles[0].fire() - await runtime.close() - - attempts = state.list_attempts() - assert len(attempts) == 1 - assert attempts[0]["outcome"] == "cancelled_after_fire" - assert attempts[0]["mail_watermark"] is None - - -@pytest.mark.asyncio -async def test_restart_closes_interrupted_timer_attempt_as_delivery_unknown( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - state.begin_attempt( - attempt_id="attempt:crashed", - timer_id="timer:crashed", - scheduled_for=now, - fired_at=now, - ) - state.set_attempt_mail_watermark(attempt_id="attempt:crashed", mail_watermark=5) - future = now + timedelta(hours=1) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, _Content(future, {"kind": "future"})), - cast(DriftWakeServices, _Drift(future, None)), - state=WakeState(state.path), - now=lambda: now, - ) - - await runtime.start() - - attempt = state.get_attempt("attempt:crashed") - assert attempt is not None - assert attempt["outcome"] == "delivery_unknown" - assert attempt["detail"] == "进程重启前检查未闭合,外部效果未知" - assert state.count_attempts() == 1 - await runtime.close() - - -@pytest.mark.asyncio -async def test_below_threshold_content_is_recorded_for_real_timer_fire( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, timers := _Timers()), - cast(PluginScopedTurns, turns := _Turns()), - cast(ContentWakeServices, _Content(now, {"preprocess_score": 0.001})), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - ) - await runtime.start() - await asyncio.sleep(0) - - timers.handles[0].fire() - for _ in range(10): - await asyncio.sleep(0) - attempts = state.list_attempts() - if attempts and attempts[0]["outcome"] == "content_insufficient": - break - - assert turns.starts == [] - assert state.list_attempts()[0]["outcome"] == "content_insufficient" - assert state.list_attempts()[0]["owner"] == "content" - detail = str(state.list_attempts()[0]["detail"]) - assert all( - field in detail - for field in ( - "active=", - "due=", - "expired=", - "scored=", - "new=", - "new_mass=", - "pool_mass=", - "threshold=", - "below_floor=", - "driver=", - ) - ) - await runtime.close() - - -@pytest.mark.asyncio -async def test_below_threshold_content_stays_pending_without_repeated_check( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"preprocess_score": 0.3}) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(tmp_path / "wake.sqlite3"), - now=lambda: now, - ) - - first = await runtime._admit_attempt() - second = await runtime._admit_attempt() - - assert first.outcome == "content_insufficient" - assert second.outcome == "content_insufficient" - assert "没有新 Content" in second.detail - assert len(content.snapshot(now)["items"]) == 1 - - -@pytest.mark.asyncio -async def test_due_timer_starts_memory_aware_screen_turn(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content( - now, - { - "kind": "due", - "preprocess_score": 0.9, - "published_at": now.isoformat(), - }, - ) - drift = _Drift(now, None) - runtime, timers, turns = _runtime( - now, content, drift, state=WakeState(tmp_path / "wake.sqlite3") - ) - await runtime.start() - await asyncio.sleep(0) - timers.handles[0].fire() - await asyncio.wait_for(turns.started.wait(), timeout=1) - - assert len(turns.starts) == 1 - start = turns.starts[0] - assert start["channel"] == "wake" - scope = start["scope"] - assert scope.storage is TurnStorage.IN_MEMORY - assert scope.post_commit_effect is PostCommitEffect.SUPPRESS - assert scope.disabled_prompt_sections == frozenset() - assert scope.tool_grant.allows("message_push") is False - assert scope.tool_grant.allows("tool_search") is False - assert scope.tool_grant.allows("screen_content") is True - assert scope.tool_grant.allows("share_content") is False - assert scope.terminal_tools == ("screen_content",) - assert scope.max_iterations == 1 - await runtime.close() - - -@pytest.mark.asyncio -async def test_source_report_from_worker_thread_wakes_runtime(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - runtime, _timers, _turns = _runtime( - now, - _Content(now), - _Drift(now), - ) - runtime._loop = asyncio.get_running_loop() - content = cast(_Content, runtime._content) - - waiter = asyncio.create_task(runtime._dirty.wait()) - await asyncio.to_thread( - content.report_alert, - source_id="fixture", - event_id="worker", - payload={"title": "worker report"}, - observed_at=now, - ) - runtime.content_changed() - - await asyncio.wait_for(waiter, timeout=1) - - -@pytest.mark.asyncio -async def test_context_source_can_report_before_wake_runtime_starts(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - state = WakeState(tmp_path / "wake.sqlite3") - content = _Content( - now, - { - "title": "Candidate", - "preprocess_score": 0.9, - "published_at": now.isoformat(), - }, - ) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - ) - await asyncio.to_thread( - content.report_context, - source_id="steam", - event_id="current", - payload={"presence": "in_game"}, - observed_at=now, - expires_at=now + timedelta(minutes=10), - ) - runtime.content_changed() - await runtime.start() - admission = await runtime._admit_attempt() - assert admission.turn_owner == "content" - runtime._active_owner = "content" - await runtime.prepare(_ctx(now)) - runtime._screened_content = ( - _ScreenedItem(_CONTENT_CANDIDATE, "likely", "Confirm?"), - ) - runtime._phase = "content_investigate" - second = _ctx(now) - second.turn_id = "turn:2" - await runtime.prepare(second) - - assert content.active_context(now)[0]["payload"] == {"presence": "in_game"} - assert '"presence":"in_game"' in second.extra_hints[0] - await runtime.close() - - -@pytest.mark.asyncio -async def test_expired_prepared_alert_is_cancelled_before_restart_send( - tmp_path, -) -> None: - selected_at = datetime(2026, 8, 23, 9, tzinfo=UTC) - recovered_at = selected_at + timedelta(minutes=2) - state = WakeState(tmp_path / "wake.sqlite3") - content = _Content(selected_at) - content.report_alert( - source_id="calendar", - event_id="meeting:expired", - payload={"title": "Old meeting"}, - observed_at=selected_at, - expires_at=selected_at + timedelta(minutes=1), - ) - accepted = TurnAcceptedReceipt("wake:default", "turn:expired") - assert ( - content.select_alert( - {"session_id": accepted.session_id, "turn_id": accepted.turn_id}, - selected_at, - ) - is not None - ) - ledger = DurableDeliveryStore(tmp_path / "settlements.sqlite") - _ = ledger.prepare( - { - "logical_delivery_id": "wake:alert:expired", - "accepted_session_id": accepted.session_id, - "accepted_turn_id": accepted.turn_id, - "target_service": EVENTMAIL_ALERT_DELIVERY.name, - "channel": "recording", - "recipient": "recipient", - "projection_session_id": "recipient-session", - "body": "Do not send", - "metadata": { - "source_id": "calendar", - "event_id": "meeting:expired", - }, - } - ) - sender_calls = 0 - - async def sender(*_args: object) -> object: - nonlocal sender_calls - sender_calls += 1 - raise AssertionError("expired prepared Alert reached provider sender") - - async def projector(_request: object) -> str: - raise AssertionError("expired prepared Alert reached Session projector") - - deliveries = PluginDurableDeliveries( - ledger, - cast(Any, sender), - projector, - recover_started=False, - ) - turns = _Turns() - turns.reads[accepted] = DurableTurnView( - accepted.session_id, - accepted.turn_id, - TurnStatus.COMPLETED, - "share", - None, - None, - None, - (), - ) - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(recovered_at)), - deliveries=deliveries, - content_delivery=cast(Any, SimpleNamespace(pending=lambda _limit: ())), - drift_delivery=cast(Any, SimpleNamespace(pending=lambda _limit: ())), - target=DeliveryTarget( - channel="recording", - recipient="recipient", - session_id="recipient-session", - ), - state=state, - now=lambda: recovered_at, - ) - - await runtime.start() - - assert sender_calls == 0 - assert content.alert_status("calendar", "meeting:expired") == "expired" - delivery = deliveries.lookup(accepted) - assert delivery is not None and delivery.state == "rejected" - await runtime.close() - - -@pytest.mark.asyncio -async def test_content_crash_before_selection_retries_then_commits(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content( - now, - { - "title": "High value", - "preprocess_score": 0.9, - "published_at": now.isoformat(), - }, - ) - path = tmp_path / "wake.sqlite3" - first_state = WakeState(path) - first = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=first_state, - now=lambda: now, - ) - - admission = await first._admit_attempt() - assert admission.turn_owner == "content" - assert first_state.has_unseen_due(content.snapshot(now)["items"], now) is True - - recovered_state = WakeState(path) - recovered = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=recovered_state, - now=lambda: now, - ) - admission = await recovered._admit_attempt() - assert admission.turn_owner == "content" - recovered._active_owner = "content" - await recovered.prepare(_ctx(now)) - recovered._screened_content = ( - _ScreenedItem(_CONTENT_CANDIDATE, "likely_interesting", "Confirm?"), - ) - recovered._phase = "content_investigate" - second = _ctx(now) - second.turn_id = "turn:2" - await recovered.prepare(second) - - assert recovered_state.has_unseen_due(content.snapshot(now)["items"], now) is False - - -@pytest.mark.asyncio -async def test_low_value_content_batch_does_not_admit_scoped_turn(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"preprocess_score": 0.001}) - turns = _Turns() - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(tmp_path / "wake.sqlite3"), - now=lambda: now, - ) - - admission = await runtime._admit_attempt() - assert admission.turn_owner is None - - -@pytest.mark.asyncio -async def test_passive_semantic_interest_can_admit_low_preprocess_content( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - - class RankedContent(_Content): - def snapshot(self, _now): - items = tuple( - { - "ref": { - "source_id": "fixture", - "item_id": item_id, - "revision": "1", - "state_version": 1, - }, - "payload": payload, - "snapshot_seq": index, - "status": "pending", - "observed_at": now.isoformat(), - "not_before": now.isoformat(), - "due": True, - } - for index, (item_id, payload) in enumerate( - ( - ( - "generic", - { - "title": "generic headline", - "preprocess_score": 0.2, - "published_at": now.isoformat(), - }, - ), - ( - "matched", - { - "title": "matched memory topic", - "preprocess_score": 0.001, - "published_at": now.isoformat(), - }, - ), - ), - start=1, - ) - ) - return {"snapshot_seq": 2, "items": items} - - content = RankedContent(now) - - class SemanticInterest: - async def score(self, texts, *, cutoff): - assert texts == ["generic headline", "matched memory topic"] - assert cutoff == now.isoformat() - return (0.0, 0.999) - - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(tmp_path / "wake.sqlite3"), - now=lambda: now, - semantic_interest=cast(Any, SemanticInterest()), - ) - - admission = await runtime._admit_attempt() - assert admission.turn_owner == "content" - runtime._active_owner = "content" - ctx = _ctx(now) - await runtime.prepare(ctx) - candidates = json.loads(ctx.extra_hints[0].split("候选:\n", 1)[1]) - assert candidates[0]["title"] == "matched memory topic" - - -@pytest.mark.asyncio -async def test_content_semantic_interest_is_calculated_only_once_per_revision( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content( - now, - { - "title": "one-time score", - "preprocess_score": 0.1, - "published_at": now.isoformat(), - }, - ) - - class SemanticInterest: - calls = 0 - - async def score(self, texts, *, cutoff): - assert texts == ["one-time score"] - assert cutoff == now.isoformat() - self.calls += 1 - return (0.1,) - - semantic = SemanticInterest() - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=WakeState(tmp_path / "wake.sqlite3"), - now=lambda: now, - semantic_interest=cast(Any, semantic), - ) - - first = await runtime._admit_attempt() - second = await runtime._admit_attempt() - - assert first.outcome == "content_insufficient" - assert second.outcome == "content_insufficient" - assert semantic.calls == 1 - - -@pytest.mark.asyncio -async def test_static_confidence_is_stored_then_only_time_decay_changes_mass( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now, {"title": "undated", "preprocess_score": 0.9}) - state = WakeState(tmp_path / "wake.sqlite3") - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, _Turns()), - cast(ContentWakeServices, content), - cast(DriftWakeServices, _Drift(now)), - state=state, - now=lambda: now, - ) - - assert (await runtime._admit_attempt()).outcome == "content_insufficient" - scored = state.scored_items(content.snapshot(now)["items"]) - payload = cast(Mapping[str, object], scored[0]["payload"]) - assert payload["_wake_initial_score"] == pytest.approx( - -math.log1p(-0.9) * 0.03 - ) - assert state.audit_pool(scored, now=now).pool_mass == pytest.approx( - -math.log1p(-0.9) * 0.03 - ) - assert state.audit_pool(scored, now=now + timedelta(hours=72)).pool_mass == 0.0 - - -@pytest.mark.asyncio -async def test_targeted_wake_turn_reads_mobile_history_without_writing_memory() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - turns = _Turns() - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, _Content(now)), - cast(DriftWakeServices, _Drift(now)), - target=DeliveryTarget( - channel="mobile", - recipient="device:one", - session_id="mobile:conversation", - ), - now=lambda: now, - ) - - with pytest.raises(RuntimeError, match="缺少 durable capability"): - await runtime._start_turn() - - start = turns.starts[0] - scope = start["scope"] - assert start["session_id"] == "mobile:conversation" - assert scope.storage is TurnStorage.IN_MEMORY - assert scope.session_history_read is True - assert scope.disabled_prompt_sections == frozenset() - assert scope.post_commit_effect is PostCommitEffect.SUPPRESS - - -@pytest.mark.asyncio -async def test_content_second_turn_removes_memory_and_keeps_evidence_tools() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - turns = _Turns() - runtime = WakeRuntime( - cast(PluginTimers, _Timers()), - cast(PluginScopedTurns, turns), - cast(ContentWakeServices, _Content(now, {"title": "Model update"})), - cast(DriftWakeServices, _Drift(now)), - now=lambda: now, - ) - runtime._admitted_content = ( - 1, - tuple(_Content(now, {"title": "Model update"}).snapshot(now)["items"]), - ) - runtime._active_owner = "content" - await runtime.prepare(_ctx(now)) - - await runtime._start_turn("content") - - assert len(turns.starts) == 2 - screen_scope = turns.starts[0]["scope"] - evidence_scope = turns.starts[1]["scope"] - assert screen_scope.disabled_prompt_sections == frozenset() - assert evidence_scope.disabled_prompt_sections == frozenset( - {"memory", "long_term_memory"} - ) - assert evidence_scope.preloaded_tools == ( - "recall_memory", - "web_fetch", - "share_content", - "skip_content", - ) - - -@pytest.mark.parametrize( - ("status", "retryable", "action"), - [ - (TurnStatus.COMPLETED, None, "ready_for_delivery"), - (TurnStatus.FAILED, True, "defer"), - (TurnStatus.FAILED, False, "invalidated"), - (TurnStatus.CANCELLED, None, "defer"), - (TurnStatus.INTERRUPTED, None, "defer"), - ], -) -def test_terminal_matrix(status, retryable, action) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - drift = _Drift(now) - runtime, _timers, _turns = _runtime(now, content, drift) - error = TurnError("fixture", "failed", retryable) if retryable is not None else None - view = DurableTurnView( - "wake:default", - "turn:1", - status, - None, - error.type if error else None, - error.message if error else None, - error.retryable if error else None, - ( - ( - _decision_item( - "share_content", - {"message": "hello", "items": [_CONTENT_CANDIDATE]}, - ), - ) - if status is TurnStatus.COMPLETED - else () - ), - ) - - runtime._settle( - "content", - _content_receipt(), - view, - ) - - assert content.transitions[0][1] == action - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("decision", "arguments", "expected_action", "expected_timers"), - [ - ( - "share_content", - {"message": "done", "items": [_CONTENT_CANDIDATE]}, - "ready_for_delivery", - 1, - ), - ("skip_content", {"reason": "not relevant"}, "release", 1), - (None, {}, "defer", 1), - ], -) -async def test_startup_reconciles_durable_typed_decision_before_arming( - tmp_path, - decision: str | None, - arguments: dict[str, object], - expected_action: str, - expected_timers: int, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - content.selected_rows = [ - { - "selection_token": "content:selection", - "status": "selected", - "accepted_turn": { - "session_id": "wake:default", - "turn_id": "turn:old", - }, - "items": ({"ref": dict(_CONTENT_REF), "payload": {}},), - } - ] - drift = _Drift(now) - runtime, timers, turns = _runtime( - now, content, drift, state=WakeState(tmp_path / "wake.sqlite3") - ) - accepted = TurnAcceptedReceipt("wake:default", "turn:old") - turns.reads[accepted] = DurableTurnView( - "wake:default", - "turn:old", - TurnStatus.COMPLETED, - "过滤,不推送(事故诱饵)", - None, - None, - None, - (() if decision is None else (_decision_item(decision, arguments),)), - ) - - await runtime.start() - await asyncio.sleep(0) - - assert content.transitions[0][1] == expected_action - assert len(timers.handles) == expected_timers - await runtime.close() - - -@pytest.mark.parametrize( - ("items", "action"), - [ - ((_decision_item("skip_content", {"reason": "not relevant"}),), "release"), - ((), "defer"), - ( - ( - _decision_item("share_content", {"message": "share"}), - _decision_item("skip_content", {"reason": "conflict"}), - ), - "defer", - ), - ( - (_decision_item("share_content", {"message": "share", "items": []}),), - "defer", - ), - ( - ( - _decision_item( - "share_content", - {"message": "share", "items": ["candidate_unknown"]}, - ), - ), - "defer", - ), - ( - ( - _decision_item( - "share_content", - { - "message": "share", - "items": [_CONTENT_CANDIDATE, _CONTENT_CANDIDATE], - }, - ), - ), - "defer", - ), - ], -) -def test_completed_content_requires_one_structured_decision( - items: tuple[TurnItem, ...], action: str -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - runtime, _timers, _turns = _runtime(now, content, _Drift(now)) - - runtime._settle( - "content", - _content_receipt(), - DurableTurnView( - "wake:default", - "turn:decision", - TurnStatus.COMPLETED, - "internal diagnostic text", - None, - None, - None, - items, - ), - ) - - assert content.transitions[0][1] == action - - -@pytest.mark.parametrize( - ("items", "action"), - [ - ( - ( - _decision_item( - "share_content", {"message": "drift thought", "items": []} - ), - ), - "ready_for_delivery", - ), - ((_decision_item("skip_content", {"reason": "stay quiet"}),), "await_change"), - ((), "await_change"), - ], -) -def test_completed_drift_uses_same_typed_delivery_decision( - items: tuple[TurnItem, ...], action: str -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - drift = _Drift(now) - runtime, _timers, _turns = _runtime(now, _Content(now), drift) - - runtime._settle( - "drift", - {"selection_token": "drift:selection", "next_due": None}, - DurableTurnView( - "wake:default", - "turn:drift", - TurnStatus.COMPLETED, - "过滤,不推送(事故诱饵)", - None, - None, - None, - items, - ), - ) - - assert drift.transitions[0][1] == action - - -@pytest.mark.asyncio -async def test_startup_active_selection_fails_loud_without_timer_or_second_turn( - tmp_path, -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - content.selected_rows = [ - { - "selection_token": "content:active", - "status": "selected", - "accepted_turn": { - "session_id": "wake:default", - "turn_id": "turn:active", - }, - } - ] - drift = _Drift(now) - runtime, timers, turns = _runtime( - now, content, drift, state=WakeState(tmp_path / "wake.sqlite3") - ) - accepted = TurnAcceptedReceipt("wake:default", "turn:active") - turns.reads[accepted] = DurableTurnView( - "wake:default", - "turn:active", - TurnStatus.IN_PROGRESS, - None, - None, - None, - None, - ) - - with pytest.raises(RuntimeError, match="早于 Core Turn recovery/handoff"): - await runtime.start() - - assert content.selected_rows[0]["selection_token"] == "content:active" - assert content.transitions == [] - assert timers.handles == [] and turns.starts == [] - await runtime.close() - - -@pytest.mark.parametrize( - ("next_due", "action"), - [ - ("2026-08-23T09:05:00+00:00", "defer"), - (None, "await_change"), - ], -) -@pytest.mark.parametrize( - "status", - [TurnStatus.FAILED, TurnStatus.CANCELLED, TurnStatus.INTERRUPTED], -) -def test_drift_retry_transition_respects_proposal_owned_next_due( - status, next_due, action -) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - drift = _Drift(now) - runtime, _timers, _turns = _runtime(now, content, drift) - view = DurableTurnView( - "wake:default", - "turn:drift", - status, - None, - "fixture" if status is TurnStatus.FAILED else None, - "retry" if status is TurnStatus.FAILED else None, - True if status is TurnStatus.FAILED else None, - ) - - runtime._settle( - "drift", - {"selection_token": "drift:selection", "next_due": next_due}, - view, - ) - - assert drift.transitions == [("drift:selection", action)] - - -def test_startup_transition_rejection_fails_loud_instead_of_looping() -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - drift = _Drift(now) - runtime, _timers, _turns = _runtime(now, content, drift) - view = DurableTurnView( - "wake:default", - "turn:1", - TurnStatus.COMPLETED, - "done", - None, - None, - None, - ) - - def rejected(token, action, *, not_before=None, selected_refs=None): - return {"changed": False, "reason": "status:ready_for_delivery"} - - content.transition = rejected - with pytest.raises(RuntimeError, match="selected transition 未提交"): - runtime._settle( - "content", - {"selection_token": "content:selection"}, - view, - ) - - -@pytest.mark.asyncio -async def test_startup_reconciles_more_than_one_selected_page(tmp_path) -> None: - now = datetime(2026, 8, 23, 9, tzinfo=UTC) - content = _Content(now) - content.selected_rows = [ - { - "selection_token": f"content:{index}", - "status": "selected", - "accepted_turn": { - "session_id": "wake:default", - "turn_id": f"turn:{index}", - }, - } - for index in range(101) - ] - drift = _Drift(now) - runtime, _timers, turns = _runtime( - now, content, drift, state=WakeState(tmp_path / "wake.sqlite3") - ) - for index in range(101): - accepted = TurnAcceptedReceipt("wake:default", f"turn:{index}") - turns.reads[accepted] = DurableTurnView( - "wake:default", - f"turn:{index}", - TurnStatus.COMPLETED, - "done", - None, - None, - None, - ) - - await runtime.start() - - assert content.selected_rows == [] - assert len(content.transitions) == 101 - await runtime.close() diff --git a/tests/test_wake_v3_composition.py b/tests/test_wake_v3_composition.py deleted file mode 100644 index 4dbfa42f7..000000000 --- a/tests/test_wake_v3_composition.py +++ /dev/null @@ -1,754 +0,0 @@ -from __future__ import annotations - -import asyncio -import shutil -from collections.abc import Sequence -from datetime import UTC, datetime, timedelta -from pathlib import Path -from typing import cast - -import pytest - -import agent.plugins.manager as plugin_manager_module -from agent.control.models import TurnItem, TurnItemKind, TurnRequest, TurnStatus -from agent.control.ports import ControlExecutionResult -from agent.control.runtime import ConversationRuntime -from agent.control.scoped_turn import TurnAcceptedReceipt -from agent.control.timer import TimerReceipt, TimerStatus -from agent.lifecycle.composition import ( - CONTEXT_PREPARED_EVENT, - run_composition_lifecycle, -) -from agent.lifecycle.types import BeforeTurnCtx -from agent.plugin_composition.channels import ChannelDeliveryReceipt, DeliveryStatus -from agent.plugin_composition.durable_deliveries import ( - DurableBindingAttempt, - PluginDurableDeliveries, -) -from agent.plugin_composition.durable_delivery_store import DurableDeliveryStore -from agent.plugins.manager import PluginManager -from agent.plugins.snapshot import bind_runtime_snapshot, reset_runtime_snapshot -from agent.tools.registry import ToolRegistry -from bus.event_bus import EventBus -from plugins.eventmail.plugin import EVENTMAIL_CONTENT_SOURCE, EVENTMAIL_WAKE -from plugins.eventmail.store import EventMailStore -from plugins.drift.plugin import DRIFT_PROPOSALS, DRIFT_WAKE -from plugins.wake.plugin import _candidate_id, _message_with_source_links -from session.manager import SessionManager -from session.store import SessionStore - - -class _TimerHandle: - def __init__(self, deadline: datetime) -> None: - self.deadline = deadline - self.future: asyncio.Future[TimerReceipt] = ( - asyncio.get_running_loop().create_future() - ) - - @property - def id(self) -> str: - return "timer:wake:e2e" - - async def result(self) -> TimerReceipt: - return await asyncio.shield(self.future) - - async def cancel(self) -> TimerReceipt: - if not self.future.done(): - self.future.set_result( - TimerReceipt( - self.id, self.deadline, datetime.now(UTC), TimerStatus.CANCELLED - ) - ) - return await self.future - - async def cleanup(self) -> None: - _ = await self.cancel() - - def fire(self) -> None: - self.future.set_result( - TimerReceipt(self.id, self.deadline, datetime.now(UTC), TimerStatus.FIRED) - ) - - -class _Timer: - def __init__(self) -> None: - self.handles: list[_TimerHandle] = [] - - def schedule(self, deadline: datetime) -> _TimerHandle: - handle = _TimerHandle(deadline) - self.handles.append(handle) - return handle - - -async def _eventually(predicate) -> None: - for _ in range(300): - if predicate(): - return - await asyncio.sleep(0.01) - raise AssertionError("condition did not settle") - - -def _copy_plugins(tmp_path: Path) -> list[Path]: - root = Path(__file__).resolve().parents[1] - paths = [] - for name in ("eventmail", "drift", "wake"): - target = tmp_path / "plugins" / name - shutil.copytree(root / "plugins" / name, target) - paths.append(target) - semantic_provider = tmp_path / "plugins" / "semantic_provider" - semantic_provider.mkdir() - (semantic_provider / "plugin.py").write_text( - """from agent.plugin_composition import CONVERSATION_SEMANTIC_INTEREST - -api_version = 3 -name = "semantic_provider" -version = "1.0.0" -inject = () - -class SemanticInterest: - async def score(self, texts, *, cutoff): - return tuple(0.0 for _ in texts) - -async def apply(ctx, config): - await ctx.provide(CONVERSATION_SEMANTIC_INTEREST, SemanticInterest()) -""", - encoding="utf-8", - ) - paths.append(semantic_provider) - memory_recall = tmp_path / "plugins" / "memory_recall" - shutil.copytree(root / "tests" / "fixtures" / "memory_recall", memory_recall) - paths.append(memory_recall) - return paths - - -def test_wake_source_links_keep_order_and_do_not_duplicate_existing_url() -> None: - body = _message_with_source_links( - "First source is already cited: https://example.test/one", - { - "source_refs": [ - {"title": "One", "url": "https://example.test/one"}, - {"title": "Two\nsource", "url": "https://example.test/two"}, - {"title": "Unsafe", "url": "javascript:alert(1)"}, - ] - }, - ) - - assert body == ( - "First source is already cited: https://example.test/one\n\n" - "来源:\n- Two source:" - ) - - -@pytest.mark.asyncio -async def test_wake_fails_loud_without_semantic_interest_provider( - tmp_path: Path, -) -> None: - root = Path(__file__).resolve().parents[1] - plugin_dirs: list[Path] = [] - for name in ("eventmail", "drift", "wake"): - target = tmp_path / "plugins" / name - shutil.copytree(root / "plugins" / name, target) - plugin_dirs.append(target) - workspace = tmp_path / "workspace" - sessions = SessionManager(workspace) - manager = PluginManager( - plugin_dirs=plugin_dirs, - event_bus=EventBus(), - workspace=workspace, - session_manager=sessions, - tool_registry=ToolRegistry(), - installed_cache_root=tmp_path / "cache", - ) - try: - with pytest.raises(RuntimeError, match="conversation.semantic_interest"): - await manager.load_all() - finally: - await manager.terminate_all() - sessions.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ( - "decision_name", - "decision_arguments", - "decision_status", - "expected_content_counts", - "expected_body", - ), - [ - ( - "share_content", - { - "message": "fixture share body", - "items": [ - _candidate_id( - { - "source_id": "fitbit-e2e", - "item_id": "sleep:e2e", - "revision": "1", - } - ) - ], - }, - "success", - {"settled": 1}, - ( - "fixture share body\n\n来源:\n" - "- Fixture sleep source:" - ), - ), - ( - "skip_content", - {"reason": "fixture candidate is irrelevant"}, - "success", - {"pending": 1}, - None, - ), - (None, {}, None, {"deferred": 1}, None), - ( - "share_content", - {"message": " ", "items": []}, - "error", - {"deferred": 1}, - None, - ), - ( - "skip_content", - {"reason": "\t"}, - "error", - {"deferred": 1}, - None, - ), - ], -) -async def test_real_wake_plugin_uses_typed_decision_not_model_response( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - decision_name: str | None, - decision_arguments: dict[str, object], - decision_status: str | None, - expected_content_counts: dict[str, int], - expected_body: str | None, -) -> None: - timer = _Timer() - monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", lambda: timer) - workspace = tmp_path / "workspace" - workspace.mkdir() - wake_data = workspace / "plugin-data" / "wake-builtin" - wake_data.mkdir(parents=True) - (wake_data / "config.local.toml").write_text( - """ -[delivery] -channel = "recording" -recipient = "recipient:one" -session_id = "recipient-session" -""".strip() + "\n", - encoding="utf-8", - ) - store = SessionStore(workspace / "sessions.db") - sessions = SessionManager(workspace) - provider_calls: list[str] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - turn_id = request.metadata["turnId"] - assert isinstance(turn_id, str) - ctx = BeforeTurnCtx( - session_key=request.thread_id, - channel=str(request.metadata["channel"]), - chat_id=str(request.metadata["chatId"]), - content=request.input, - timestamp=datetime.now(UTC), - history_messages=(), - turn_id=turn_id, - ) - await run_composition_lifecycle(CONTEXT_PREPARED_EVENT, ctx) - if ctx.extra_hints[0].startswith("【Wake Content 初筛】"): - tool_name = "screen_content" - tool_status = "success" - tool_arguments: dict[str, object] = { - "items": [ - { - "candidate_id": _candidate_id( - { - "source_id": "fitbit-e2e", - "item_id": "sleep:e2e", - "revision": "1", - } - ), - "initial_interest": "likely_interesting", - "question": "Does this match the user's preference?", - } - ] - } - else: - tool_name = decision_name - tool_status = decision_status - tool_arguments = decision_arguments - items = [] if tool_name is None else [ - TurnItem( - TurnItemKind.TOOL_CALL, - f"item:{tool_name}", - { - "callId": f"call:{tool_name}", - "name": tool_name, - "status": tool_status, - "arguments": tool_arguments, - "resultPreview": '{"recorded":true}', - }, - ) - ] - return ControlExecutionResult( - response="过滤,不推送(事故诱饵,绝不能发给用户)", - items=items, - ) - - async def deliver(request, provider_started): - provider_started( - DurableBindingAttempt( - request.logical_delivery_id, - "snapshot:recording", - "generation:recording", - "binding:recording", - ) - ) - provider_calls.append(request.body) - return ChannelDeliveryReceipt( - request.logical_delivery_id, - DeliveryStatus.DELIVERED, - ("provider:recording",), - ) - - conversation = ConversationRuntime(store, execute) - manager = PluginManager( - plugin_dirs=_copy_plugins(tmp_path), - event_bus=EventBus(), - workspace=workspace, - session_manager=sessions, - tool_registry=ToolRegistry(), - installed_cache_root=tmp_path / "cache", - ) - manager.bind_conversation_runtime( - conversation, - programmatic_session_creator=store.create_session, - programmatic_session_reader=store.get_session_meta, - ) - manager.bind_durable_delivery_sender(deliver) - await manager.load_all() - if decision_status == "error": - registry = manager.current_snapshot.tool_registry - assert registry is not None and decision_name is not None - lease = manager.snapshot_store.lease() - snapshot_token = bind_runtime_snapshot(lease) - try: - registry.set_context( - origin_channel="wake", - origin_session_key="recipient-session", - turn_id="turn:boundary", - ) - with pytest.raises(ValueError, match="必须是非空字符串"): - await registry.execute( - decision_name, - decision_arguments, - raise_errors=True, - ) - finally: - reset_runtime_snapshot(snapshot_token) - await lease.release() - root = manager.current_snapshot.composition_root - assert root is not None - source = root.context.require(EVENTMAIL_CONTENT_SOURCE).bind("fitbit-e2e") - _ = source.submit( - "poll:e2e", - ( - { - "item_id": "sleep:e2e", - "revision": "1", - "payload": { - "kind": "sleep", - "preprocess_score": 0.9, - "published_at": datetime.now(UTC).isoformat(), - "title": "Fixture sleep source", - "url": "https://example.test/sleep/e2e", - }, - "not_before": datetime.now(UTC), - "requires_ack": False, - }, - ), - ) - ledger = DurableDeliveryStore( - workspace / "runtime" / "deliveries" / "settlements.sqlite" - ) - content_store = EventMailStore( - workspace / "plugin-data" / "eventmail-builtin" / "eventmail.sqlite3" - ) - lifecycle = asyncio.create_task(manager.run_runtime_services()) - try: - await _eventually(lambda: len(timer.handles) == 2) - min(timer.handles, key=lambda handle: handle.deadline).fire() - await _eventually( - lambda: len(store.list_turns("recipient-session")) == 2 - and all( - turn.status is TurnStatus.COMPLETED - for turn in store.list_turns("recipient-session") - ) - ) - await _eventually( - lambda: content_store.state_counts() == expected_content_counts - ) - - turns = store.list_turns("recipient-session") - assert len(turns) == 2 - decision_turn = next( - ( - turn - for turn in turns - if any( - item.kind is TurnItemKind.TOOL_CALL - and item.data.get("name") in {"share_content", "skip_content"} - for item in turn.items - ) - ), - turns[0], - ) - accepted = TurnAcceptedReceipt("recipient-session", decision_turn.id) - delivery = PluginDurableDeliveries(ledger, None, None, recover_started=False) - view = delivery.lookup(accepted) - messages = sessions.control_store.fetch_session_messages("recipient-session") - if expected_body is None: - assert provider_calls == [] - assert messages == [] - assert view is None - else: - assert provider_calls == [expected_body] - assert len(messages) == 1 - assert messages[0]["content"] == expected_body - assert messages[0]["control_turn_id"] == decision_turn.id - assert messages[0]["tools_used"] == ["message_push"] - assert messages[0]["evidence_item_ids"] == ["fitbit-e2e:sleep:e2e:1"] - assert messages[0]["source_refs"] == [ - { - "display_index": 1, - "event_id": "fitbit-e2e:sleep:e2e:1", - "title": "Fixture sleep source", - "url": "https://example.test/sleep/e2e", - } - ] - assert messages[0]["state_summary_tag"] == "none" - assert view is not None and view.state == "settled" - assert all("事故诱饵" not in message["content"] for message in messages) - finally: - lifecycle.cancel() - _ = await asyncio.gather(lifecycle, return_exceptions=True) - await manager.terminate_all() - await conversation.shutdown() - sessions.close() - store.close() - - -@pytest.mark.asyncio -async def test_wake_candidate_has_zero_timer_turn_and_formal_domain_write( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - timer = _Timer() - monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", lambda: timer) - workspace = tmp_path / "workspace" - workspace.mkdir() - store = SessionStore(workspace / "sessions.db") - executions: list[TurnRequest] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - executions.append(request) - return ControlExecutionResult(response="unexpected") - - conversation = ConversationRuntime(store, execute) - plugin_dirs = _copy_plugins(tmp_path) - manager = PluginManager( - plugin_dirs=plugin_dirs, - event_bus=EventBus(), - workspace=workspace, - installed_cache_root=tmp_path / "cache", - ) - manager.bind_conversation_runtime( - conversation, - programmatic_session_creator=store.create_session, - programmatic_session_reader=store.get_session_meta, - ) - await manager.load_all() - root = manager.current_snapshot.composition_root - assert root is not None - now = datetime.now(UTC) - source = root.context.require(EVENTMAIL_CONTENT_SOURCE).bind("candidate-source") - _ = source.submit( - "batch:candidate", - ( - { - "item_id": "content:candidate", - "revision": "1", - "payload": {"kind": "candidate"}, - "not_before": now, - "requires_ack": False, - }, - ), - ) - _ = root.context.require(DRIFT_PROPOSALS).propose( - "drift:candidate", - "1", - {}, - now, - next_due=now + timedelta(minutes=5), - ) - before_content = root.context.require(EVENTMAIL_WAKE).snapshot(now) - before_drift = root.context.require(DRIFT_WAKE).snapshot(now) - wake_dir = next(path for path in plugin_dirs if path.name == "wake") - with (wake_dir / "plugin.py").open("a", encoding="utf-8") as handle: - handle.write("\n# candidate wake revision\n") - try: - candidate = await manager.prepare_candidate("wake") - assert candidate is not None - assert timer.handles == [] - assert executions == [] - assert root.context.require(EVENTMAIL_WAKE).snapshot(now) == before_content - assert root.context.require(DRIFT_WAKE).snapshot(now) == before_drift - await manager.discard_prepared("wake") - finally: - await manager.terminate_all() - await conversation.shutdown() - store.close() - - -@pytest.mark.asyncio -async def test_real_root_selected_content_runs_two_stage_react_and_not_drift( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - timer = _Timer() - monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", lambda: timer) - workspace = tmp_path / "workspace" - workspace.mkdir() - store = SessionStore(workspace / "sessions.db") - prepared: list[BeforeTurnCtx] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - turn_id = request.metadata["turnId"] - assert isinstance(turn_id, str) - ctx = BeforeTurnCtx( - session_key=request.thread_id, - channel=str(request.metadata["channel"]), - chat_id=str(request.metadata["chatId"]), - content=request.input, - timestamp=datetime.now(UTC), - history_messages=(), - turn_id=turn_id, - ) - await run_composition_lifecycle(CONTEXT_PREPARED_EVENT, ctx) - prepared.append(ctx) - candidate_id = _candidate_id( - { - "source_id": "e2e-source", - "item_id": "content:1", - "revision": "1", - } - ) - if ctx.extra_hints[0].startswith("【Wake Content 初筛】"): - name = "screen_content" - arguments: dict[str, object] = { - "items": [ - { - "candidate_id": candidate_id, - "initial_interest": "likely_interesting", - "question": "Is this useful?", - } - ] - } - else: - name = "skip_content" - arguments = {"reason": "fixture skip"} - return ControlExecutionResult( - response="wake response", - items=[ - TurnItem( - TurnItemKind.TOOL_CALL, - f"item:{name}", - { - "callId": f"call:{name}", - "name": name, - "status": "success", - "arguments": arguments, - "resultPreview": '{"recorded":true}', - }, - ) - ], - ) - - conversation = ConversationRuntime(store, execute) - manager = PluginManager( - plugin_dirs=_copy_plugins(tmp_path), - event_bus=EventBus(), - workspace=workspace, - installed_cache_root=tmp_path / "cache", - ) - manager.bind_conversation_runtime( - conversation, - programmatic_session_creator=store.create_session, - programmatic_session_reader=store.get_session_meta, - ) - await manager.load_all() - root = manager.current_snapshot.composition_root - assert root is not None - content = root.context.require(EVENTMAIL_CONTENT_SOURCE).bind("e2e-source") - now = datetime.now(UTC) - _ = content.submit( - "batch:1", - ( - { - "item_id": "content:1", - "revision": "1", - "payload": { - "kind": "fitbit", - "preprocess_score": 0.9, - "published_at": now.isoformat(), - }, - "not_before": now, - "requires_ack": False, - }, - ), - ) - _ = root.context.require(DRIFT_PROPOSALS).propose( - "drift:1", - "1", - {"prompt": "reflect"}, - now, - next_due=now + timedelta(minutes=5), - ) - lifecycle = asyncio.create_task(manager.run_runtime_services()) - try: - await _eventually(lambda: len(timer.handles) == 2) - min(timer.handles, key=lambda handle: handle.deadline).fire() - await _eventually( - lambda: len(store.list_turns("wake:default")) == 2 - and all( - turn.status is TurnStatus.COMPLETED - for turn in store.list_turns("wake:default") - ) - ) - wake_content = root.context.require(EVENTMAIL_WAKE) - await _eventually(lambda: wake_content.selected() == ()) - - turns = store.list_turns("wake:default") - assert len(turns) == 2 - assert [turn.final_response for turn in turns] == [ - "wake response", - "wake response", - ] - assert all(ctx.abort is False for ctx in prepared) - assert prepared[0].extra_hints[0].startswith("【Wake Content 初筛】") - assert prepared[1].extra_hints[0].startswith("【Wake Content 找证据】") - assert root.context.require(DRIFT_WAKE).selected() == () - drift_snapshot = root.context.require(DRIFT_WAKE).snapshot(datetime.now(UTC)) - assert len(cast(Sequence[object], drift_snapshot["proposals"])) == 1 - finally: - lifecycle.cancel() - _ = await asyncio.gather(lifecycle, return_exceptions=True) - await manager.terminate_all() - await conversation.shutdown() - store.close() - - -@pytest.mark.asyncio -async def test_real_root_both_decline_is_quiet_but_keeps_control_diagnostics( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - timer = _Timer() - monkeypatch.setattr(plugin_manager_module, "AsyncioOneShotTimer", lambda: timer) - workspace = tmp_path / "workspace" - workspace.mkdir() - store = SessionStore(workspace / "sessions.db") - provider_calls = 0 - prepared: list[BeforeTurnCtx] = [] - - async def execute(request: TurnRequest) -> ControlExecutionResult: - nonlocal provider_calls - turn_id = request.metadata["turnId"] - assert isinstance(turn_id, str) - ctx = BeforeTurnCtx( - session_key=request.thread_id, - channel=str(request.metadata["channel"]), - chat_id=str(request.metadata["chatId"]), - content=request.input, - timestamp=datetime.now(UTC), - history_messages=(), - turn_id=turn_id, - ) - await run_composition_lifecycle(CONTEXT_PREPARED_EVENT, ctx) - prepared.append(ctx) - if not ctx.abort: - provider_calls += 1 - return ControlExecutionResult(response="unexpected") - return ControlExecutionResult(response=ctx.abort_reply) - - conversation = ConversationRuntime(store, execute) - manager = PluginManager( - plugin_dirs=_copy_plugins(tmp_path), - event_bus=EventBus(), - workspace=workspace, - installed_cache_root=tmp_path / "cache", - ) - manager.bind_conversation_runtime( - conversation, - programmatic_session_creator=store.create_session, - programmatic_session_reader=store.get_session_meta, - ) - await manager.load_all() - root = manager.current_snapshot.composition_root - assert root is not None - now = datetime.now(UTC) - content = root.context.require(EVENTMAIL_CONTENT_SOURCE).bind("quiet-source") - _ = content.submit( - "batch:quiet", - ( - { - "item_id": "content:quiet", - "revision": "1", - "payload": {"wake_action": "decline"}, - "not_before": now, - "requires_ack": False, - }, - ), - ) - _ = root.context.require(DRIFT_PROPOSALS).propose( - "drift:quiet", - "1", - {"wake_action": "decline"}, - now, - next_due=now + timedelta(minutes=5), - ) - lifecycle = asyncio.create_task(manager.run_runtime_services()) - try: - await _eventually(lambda: len(timer.handles) == 2) - min(timer.handles, key=lambda handle: handle.deadline).fire() - await _eventually( - lambda: bool(store.list_turns("wake:default")) - and store.list_turns("wake:default")[0].status is TurnStatus.COMPLETED - ) - - turns = store.list_turns("wake:default") - assert len(turns) == 1 - assert turns[0].input == "Check durable Wake duties." - assert turns[0].final_response == "" - assert [item.kind.value for item in turns[0].items] == [ - "userMessage", - "assistantMessage", - ] - assert provider_calls == 0 - assert prepared[0].abort is True and prepared[0].abort_reply == "" - assert root.context.require(EVENTMAIL_WAKE).selected() == () - assert root.context.require(DRIFT_WAKE).selected() == () - finally: - lifecycle.cancel() - _ = await asyncio.gather(lifecycle, return_exceptions=True) - await manager.terminate_all() - await conversation.shutdown() - store.close() diff --git a/tests/test_web_shell.py b/tests/test_web_shell.py deleted file mode 100644 index a846f00ba..000000000 --- a/tests/test_web_shell.py +++ /dev/null @@ -1,500 +0,0 @@ -from __future__ import annotations - -import asyncio -import socket -import threading -import time -from pathlib import Path -from types import SimpleNamespace -from typing import Any, cast - -import pytest -import uvicorn -from fastapi.testclient import TestClient -from starlette.types import Message - -from agent.plugin_composition import ( - CapabilitySources, - ConnectionDescriptor, - ModelAvailability, - ModelCapabilities, - ModelCatalogSnapshot, - ModelDescriptor, - ModelKind, - ModelRole, - SetDefaultModel, - SettingsReceipt, -) -import bootstrap.settings_api as settings_api -import bootstrap.web_shell as web_shell -from bootstrap.chat_api import create_chat_app -from bootstrap.web_runtime import chat_socket_path, prepare_runtime_socket -from bootstrap.web_shell import create_web_shell_app -from infra.channels.web_chat_channel import WebChatChannel - - -def test_web_shell_serves_dashboard_shell_with_embedded_chat_without_config( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - project_root = tmp_path / "project" - dashboard_static = project_root / "static" / "dashboard" - chat_static = project_root / "static" / "chat" - dashboard_static.mkdir(parents=True) - chat_static.mkdir(parents=True) - (dashboard_static / "index.html").write_text( - "Akashic Dashboard", encoding="utf-8" - ) - (chat_static / "index.html").write_text( - "Akashic Chat", encoding="utf-8" - ) - module_path = project_root / "bootstrap" / "module.py" - monkeypatch.setattr(web_shell, "__file__", str(module_path)) - monkeypatch.setattr(settings_api, "__file__", str(module_path)) - - app = create_web_shell_app(tmp_path / "config.toml", tmp_path / "workspace") - - with TestClient(app) as client: - state = client.get("/api/shell/state") - shell = client.get("/") - chat = client.get("/chat") - legacy_dashboard = client.get("/dashboard") - settings = client.get("/settings") - unavailable = client.get("/api/chat/sessions") - hidden = client.post("/api/chat/model-settings/command", json={}) - rejected = client.post("/api/settings/model/command", json={}) - model_unavailable = client.post( - "/api/settings/model/command", - headers={"Origin": "http://testserver", "X-Akasic-CSRF": "1"}, - json={}, - ) - retired = client.post( - "/api/settings/roles", - headers={"Origin": "http://testserver", "X-Akasic-CSRF": "1"}, - json={}, - ) - retired_login = client.get("/api/settings/codex-login/attempt-a") - - assert state.json() == { - "status": "needs_setup", - "configured": False, - "chatReady": False, - } - assert shell.status_code == 200 - assert "Akashic Dashboard" in shell.text - assert shell.headers["cache-control"] == "no-store" - assert "script-src 'self' blob:" in shell.headers["content-security-policy"] - assert chat.status_code == 200 - assert "Akashic Chat" in chat.text - assert "img-src 'self' data: blob:" in chat.headers["content-security-policy"] - assert "connect-src 'self' data: blob:" in chat.headers["content-security-policy"] - assert legacy_dashboard.status_code == 200 - assert settings.status_code == 200 - assert "Akashic Dashboard" in settings.text - assert unavailable.status_code == 503 - assert unavailable.json()["code"] == "gateway_unavailable" - assert hidden.status_code == 404 - assert rejected.status_code == 403 - assert model_unavailable.status_code == 503 - assert retired.status_code == 410 - assert retired.json()["code"] == "model_settings_moved" - assert retired_login.status_code == 410 - - -def test_prepare_runtime_socket_replaces_only_socket_nodes(tmp_path: Path) -> None: - path = chat_socket_path(tmp_path) - path.parent.mkdir(parents=True, exist_ok=True) - server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server.bind(str(path)) - server.close() - - assert prepare_runtime_socket(path) == str(path) - assert not path.exists() - - path.write_text("not a socket", encoding="utf-8") - with pytest.raises(RuntimeError, match="非 socket"): - prepare_runtime_socket(path) - - -def test_runtime_socket_path_stays_short_for_deep_workspace(tmp_path: Path) -> None: - workspace = tmp_path.joinpath(*(["deep-workspace"] * 8)) - path = chat_socket_path(workspace) - - assert len(str(path).encode("utf-8")) < 100 - assert path.parent.resolve() == (workspace / "runtime").resolve() - - server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - server.bind(str(path)) - server.close() - assert (workspace / "runtime" / "web-chat.sock").is_socket() - - -def test_websocket_proxy_stops_when_browser_leaves_before_accept( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - class Browser: - headers: dict[str, str] = {} - close_called = False - - async def accept(self, subprotocol: str | None = None) -> None: - _ = subprotocol - raise OSError("browser left") - - async def close(self, *, code: int, reason: str) -> None: - self.close_called = True - - class Upstream: - subprotocol = None - - async def __aenter__(self) -> Upstream: - return self - - async def __aexit__(self, *args: object) -> None: - return None - - browser = Browser() - monkeypatch.setattr(web_shell, "_is_socket", lambda path: True) - monkeypatch.setattr( - web_shell.websockets, - "unix_connect", - lambda *args, **kwargs: Upstream(), - ) - - asyncio.run( - web_shell._proxy_websocket( - cast(Any, browser), - tmp_path / "gateway.sock", - "/ws", - ) - ) - - assert not browser.close_called - - -def test_dashboard_websocket_crosses_the_public_shell( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - calls: list[tuple[Path, str]] = [] - - async def proxy( - websocket: Any, - socket_path: Path, - target_path: str, - ) -> None: - calls.append((socket_path, target_path)) - await websocket.accept(subprotocol="binary") - await websocket.send_text("ready") - - monkeypatch.setattr(web_shell, "_proxy_websocket", proxy) - workspace = tmp_path / "workspace" - app = create_web_shell_app(tmp_path / "config.toml", workspace) - - with TestClient(app) as client: - with client.websocket_connect( - "/api/dashboard/computer/display?generation=computer%3A1", - subprotocols=["binary"], - ) as socket: - assert socket.accepted_subprotocol == "binary" - assert socket.receive_text() == "ready" - - assert calls == [ - ( - web_shell.dashboard_socket_path(workspace), - "/api/dashboard/computer/display?generation=computer%3A1", - ) - ] - - -def test_websocket_proxy_preserves_origin_host_and_subprotocol( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - connect_args: list[tuple[tuple[object, ...], dict[str, object]]] = [] - - class Browser: - headers = { - "host": "127.0.0.1:2237", - "origin": "http://127.0.0.1:2237", - "sec-websocket-protocol": "binary", - } - accepted_subprotocol: str | None = None - - async def accept(self, subprotocol: str | None = None) -> None: - self.accepted_subprotocol = subprotocol - - async def receive(self) -> object: - await asyncio.Event().wait() - raise AssertionError("unreachable") - - async def close(self, *, code: int, reason: str) -> None: - raise AssertionError(f"unexpected close: {code} {reason}") - - class Upstream: - subprotocol = "binary" - - async def __aenter__(self) -> Upstream: - return self - - async def __aexit__(self, *args: object) -> None: - return None - - def __aiter__(self) -> Upstream: - return self - - async def __anext__(self) -> object: - raise StopAsyncIteration - - async def close(self) -> None: - return None - - def connect(*args: object, **kwargs: object) -> Upstream: - connect_args.append((args, kwargs)) - return Upstream() - - browser = Browser() - monkeypatch.setattr(web_shell, "_is_socket", lambda path: True) - monkeypatch.setattr(web_shell.websockets, "unix_connect", connect) - - asyncio.run( - web_shell._proxy_websocket( - cast(Any, browser), - tmp_path / "dashboard.sock", - "/api/dashboard/computer/display?generation=computer%3A1", - ) - ) - - assert browser.accepted_subprotocol == "binary" - assert connect_args == [ - ( - (str(tmp_path / "dashboard.sock"),), - { - "uri": ( - "ws://127.0.0.1:2237/api/dashboard/computer/display" - "?generation=computer%3A1" - ), - "origin": "http://127.0.0.1:2237", - "subprotocols": ["binary"], - "max_size": None, - }, - ) - ] - - -def test_http_proxy_stops_when_browser_leaves_during_upload( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - class Client: - closed = False - - def build_request(self, *args: object, content: object, **kwargs: object) -> object: - return SimpleNamespace(stream=content) - - async def send(self, request: Any, *, stream: bool) -> object: - async for _ in request.stream: - pass - raise AssertionError("disconnect should stop the upload") - - async def aclose(self) -> None: - self.closed = True - - async def receive() -> dict[str, str]: - return {"type": "http.disconnect"} - - client = Client() - request = web_shell.Request( - { - "type": "http", - "method": "POST", - "path": "/", - "query_string": b"", - "headers": [], - "server": ("test", 80), - "scheme": "http", - }, - receive, - ) - monkeypatch.setattr(web_shell, "_is_socket", lambda path: True) - monkeypatch.setattr(web_shell.httpx, "AsyncClient", lambda **kwargs: client) - - response = asyncio.run( - web_shell._proxy_http(request, tmp_path / "gateway.sock", "/api/test") - ) - - assert response.status_code == 499 - assert client.closed - - -def test_http_proxy_closes_upstream_when_browser_leaves_during_response( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - class Upstream: - headers: dict[str, str] = {} - status_code = 200 - close_count = 0 - - async def aiter_raw(self): - yield b"body" - - async def aclose(self) -> None: - self.close_count += 1 - - class Client: - close_count = 0 - - def build_request(self, *args: object, **kwargs: object) -> object: - return object() - - async def send(self, request: object, *, stream: bool) -> Upstream: - return upstream - - async def aclose(self) -> None: - self.close_count += 1 - - async def receive() -> dict[str, str]: - return {"type": "http.disconnect"} - - async def send(message: Message) -> None: - if message["type"] == "http.response.body": - raise OSError("browser left") - - upstream = Upstream() - client = Client() - scope = { - "type": "http", - "method": "GET", - "path": "/", - "query_string": b"", - "headers": [], - "server": ("test", 80), - "scheme": "http", - "asgi": {"spec_version": "2.4"}, - } - request = web_shell.Request(scope, receive) - monkeypatch.setattr(web_shell, "_is_socket", lambda path: True) - monkeypatch.setattr(web_shell.httpx, "AsyncClient", lambda **kwargs: client) - - response = asyncio.run( - web_shell._proxy_http(request, tmp_path / "gateway.sock", "/api/test") - ) - asyncio.run(response(scope, receive, send)) - - assert upstream.close_count == 1 - assert client.close_count == 1 - - -def test_model_control_crosses_public_shell_and_real_chat_socket( - tmp_path: Path, -) -> None: - workspace = tmp_path / "workspace" - socket_path = chat_socket_path(workspace) - socket_path.parent.mkdir(parents=True, exist_ok=True) - applied: list[object] = [] - catalog = ModelCatalogSnapshot( - revision=4, - connections=( - ConnectionDescriptor( - connection_id="account-a", - name="Account A", - driver_id="openai-compatible", - auth_identity="account-a", - availability=ModelAvailability.AVAILABLE, - ), - ), - models=( - ModelDescriptor( - model_id="embedding-unavailable", - connection_id="account-a", - kind=ModelKind.EMBEDDING, - model="wire-embedding", - default_reasoning_effort=None, - capabilities=ModelCapabilities(embedding_dimensions=3), - capability_sources=CapabilitySources(embedding_dimensions="configured"), - availability=ModelAvailability.DISABLED, - ), - ), - role_bindings={}, - default_embedding_model_id=None, - ) - - class Control: - async def catalog(self) -> ModelCatalogSnapshot: - return catalog - - async def apply(self, command: object) -> SettingsReceipt: - applied.append(command) - return SettingsReceipt(revision=5, status="committed") - - chat_app = create_chat_app( - workspace=workspace, - channel=WebChatChannel(), - model_control=cast(Any, Control()), - ) - server = uvicorn.Server( - uvicorn.Config( - chat_app, - uds=str(socket_path), - log_level="critical", - access_log=False, - ws="none", - ) - ) - thread = threading.Thread( - target=lambda: asyncio.run(server.serve()), - name="test-chat-uds", - daemon=True, - ) - thread.start() - deadline = time.monotonic() + 5 - while not socket_path.is_socket() and thread.is_alive(): - if time.monotonic() >= deadline: - break - time.sleep(0.01) - assert socket_path.is_socket() - - try: - shell = create_web_shell_app(tmp_path / "config.toml", workspace) - with TestClient(shell) as client: - projected = client.get("/api/settings/model/catalog") - rejected_memory = client.post( - "/api/settings/memory", - headers={ - "Origin": "http://testserver", - "X-Akasic-CSRF": "1", - }, - json={ - "enabled": True, - "embedding_model_id": "embedding-unavailable", - }, - ) - changed = client.post( - "/api/settings/model/command", - headers={ - "Origin": "http://testserver", - "X-Akasic-CSRF": "1", - }, - json={ - "type": "set_default", - "expected_revision": 4, - "role": "default", - "model_id": "chat-a", - }, - ) - - assert projected.status_code == 200 - assert projected.json()["revision"] == 4 - assert "secret" not in projected.text - assert "hidden" not in projected.text - assert projected.headers["cache-control"] == "no-store" - assert rejected_memory.status_code == 404 - assert not (tmp_path / "config.toml").exists() - assert changed.status_code == 200 - assert changed.json()["revision"] == 5 - assert applied == [SetDefaultModel(4, ModelRole.DEFAULT, "chat-a")] - finally: - server.should_exit = True - thread.join(timeout=5) - assert not thread.is_alive() diff --git a/tests/test_workload_controller.py b/tests/test_workload_controller.py deleted file mode 100644 index c400a4f1f..000000000 --- a/tests/test_workload_controller.py +++ /dev/null @@ -1,922 +0,0 @@ -from __future__ import annotations - -import asyncio -import json -import os -from pathlib import Path -from urllib.parse import parse_qs, urlsplit - -import pytest - -from agent.workloads.client import UnixWorkloadController -from agent.workloads.controller import WorkloadControllerServer -from agent.workloads.model import ( - WorkloadLease, - WorkloadStartRequest, - workload_spec_digest, -) - - -class _FakeEngine: - def __init__(self) -> None: - self.container: dict[str, object] | None = None - self.create_body: dict[str, object] | None = None - self.fail_next_start = False - self.fail_next_stop = False - self.lose_create_response = False - self.crash_after_delete = False - self.owner_running: bool | None = None - self.create_count = 0 - self.delete_count = 0 - - async def request( - self, - method: str, - path: str, - *, - body: dict[str, object] | None = None, - expected: frozenset[int], - ) -> object: - _ = expected - if method == "GET" and path.startswith("/containers/json"): - if self.container is None: - return [] - if "all=0" in path and not self._running(): - return [] - return [{"Id": "container-1"}] - if method == "POST" and path.startswith("/containers/create"): - assert body is not None - self.create_count += 1 - self.create_body = body - name = parse_qs(urlsplit(path).query)["name"][0] - host = body["HostConfig"] - assert isinstance(host, dict) - mounts = host["Mounts"] - assert isinstance(mounts, list) - self.container = { - "Id": "container-1", - "Name": name, - "Config": { - "Labels": body["Labels"], - "Image": body["Image"], - "Cmd": body.get("Cmd"), - "User": body["User"], - "ExposedPorts": body["ExposedPorts"], - }, - "HostConfig": dict(host), - "NetworkSettings": {"Networks": {host["NetworkMode"]: {}}}, - "State": {"Running": False}, - "Mounts": [ - { - "Type": "bind", - "Source": value["Source"], - "Destination": value["Target"], - "RW": not value["ReadOnly"], - } - for value in mounts - ], - } - if self.lose_create_response: - self.lose_create_response = False - raise RuntimeError("create response lost") - return {"Id": "container-1"} - if method == "GET" and path == "/containers/akashic-core/json": - if self.owner_running is None: - return None - return {"State": {"Running": self.owner_running}} - if method == "GET" and path.endswith("/json"): - return self.container - if method == "POST" and path.endswith("/start"): - if self.fail_next_start: - self.fail_next_start = False - raise RuntimeError("start failed") - self._state()["Running"] = True - return None - if method == "POST" and "/stop?" in path: - if self.fail_next_stop: - self.fail_next_stop = False - raise RuntimeError("stop failed") - self._state()["Running"] = False - return None - if method == "DELETE": - self.delete_count += 1 - self.container = None - if self.crash_after_delete: - self.crash_after_delete = False - raise SystemExit("controller crashed after delete") - return None - raise AssertionError(f"unexpected Docker call: {method} {path}") - - def _state(self) -> dict[str, object]: - assert self.container is not None - state = self.container["State"] - assert isinstance(state, dict) - return state - - def _running(self) -> bool: - return self._state()["Running"] is True - - -def _request( - workspace_id: str, - generation_id: str, - *, - mode: str = "formal", - loopback: bool = False, - user_namespaces: bool = False, - limits: tuple[int, float, int] = (128, 1.0, 64), -) -> WorkloadStartRequest: - image = "example.invalid/worker@sha256:" + "b" * 64 - ports = (("gateway", 8080),) - data = (("state", "/data", True),) - health = ("gateway", "/health", 30.0) - loopback_ports = (("gateway", 18080),) if loopback else () - digest = workload_spec_digest( - plugin_id="fixture", - workload="worker", - image=image, - command=("serve",), - ports=ports, - data=data, - health=health, - limits=limits, - loopback_ports=loopback_ports, - user_namespaces=user_namespaces, - ) - return WorkloadStartRequest( - workspace_id=workspace_id, - plugin_id="fixture", - workload="worker", - mode=mode, # type: ignore[arg-type] - transaction_id=generation_id, - generation_id=generation_id, - spec_digest=digest, - image=image, - command=("serve",), - ports=ports, - data=data, - health=health, - limits=limits, - loopback_ports=loopback_ports, - user_namespaces=user_namespaces, - ) - - -def _workspace(tmp_path: Path) -> Path: - workspace = tmp_path / "workspace" - (workspace / "plugin-data").mkdir(parents=True) - (workspace / "runtime/plugin-validation").mkdir(parents=True) - return workspace - - -@pytest.mark.asyncio -async def test_controller_keeps_each_zero_limit_unlimited(tmp_path: Path) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:formal:unlimited", - limits=(0, 0.0, 0), - ) - ) - - assert fake.create_body is not None - host = fake.create_body["HostConfig"] - assert isinstance(host, dict) - assert host["Memory"] == 0 - assert host["NanoCpus"] == 0 - assert host["PidsLimit"] is None - - -@pytest.mark.asyncio -async def test_controller_uses_structured_mounts_for_colon_paths( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, "fixture:candidate:1", mode="candidate" - ) # pyright: ignore[reportPrivateUsage] - ) - - assert fake.create_body is not None - host = fake.create_body["HostConfig"] - assert isinstance(host, dict) - assert "Binds" not in host - assert host["Mounts"] == [ - { - "Type": "bind", - "Source": str( - workspace - / "runtime/plugin-validation/fixture:candidate:1" - / "workspace/plugin-data/fixture-builtin/state" - ), - "Target": "/data", - "ReadOnly": False, - } - ] - assert host["ExtraHosts"] == ["host.docker.internal:host-gateway"] - - -@pytest.mark.asyncio -async def test_controller_adds_only_the_user_namespace_seccomp_profile( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:candidate:userns", - mode="candidate", - user_namespaces=True, - ) - ) - - assert fake.create_body is not None - host = fake.create_body["HostConfig"] - assert isinstance(host, dict) - security = host["SecurityOpt"] - assert isinstance(security, list) - assert security[0] == "no-new-privileges" - assert len(security) == 2 - assert isinstance(security[1], str) - assert security[1].startswith("seccomp=") - profile = json.loads(security[1].removeprefix("seccomp=")) - assert profile["defaultAction"] == "SCMP_ACT_ERRNO" - assert any(item.get("names") == ["unshare"] for item in profile["syscalls"]) - assert any( - item.get("names") == ["chroot"] and "includes" not in item - for item in profile["syscalls"] - ) - assert "unconfined" not in security[1] - - -@pytest.mark.asyncio -async def test_controller_adopt_moves_the_only_stop_lease( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - def reject_chown(path: object, uid: int, gid: int) -> None: - raise PermissionError(f"unexpected chown: {path} {uid}:{gid}") - - monkeypatch.setattr("agent.workloads.controller.os.chown", reject_chown) - workspace = _workspace(tmp_path) - socket_path = tmp_path / "run" / "controller.sock" - server = WorkloadControllerServer( - workspace=workspace, - socket_path=socket_path, - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - task = asyncio.create_task(server.serve()) - for _ in range(100): - if socket_path.exists(): - break - await asyncio.sleep(0.01) - client = UnixWorkloadController(socket_path, timeout_seconds=5) - workspace_id = server._workspace_id # pyright: ignore[reportPrivateUsage] - - try: - first = await client.start(_request(workspace_id, "fixture:a:1")) - assert fake.create_body is not None - assert ( - fake.create_body["User"] - == f"{workspace.stat().st_uid}:{workspace.stat().st_gid}" - ) - state_dir = workspace / "plugin-data" / "fixture-builtin" / "state" - assert (state_dir.stat().st_uid, state_dir.stat().st_gid) == ( - os.getuid(), - os.getgid(), - ) - second = await client.start(_request(workspace_id, "fixture:a:2")) - - assert second.adopted_from_generation == "fixture:a:1" - with pytest.raises(RuntimeError, match="过期|adopt"): - await client.stop(first.lease) - stopped = await client.stop(second.lease) - assert stopped.container_absent - assert stopped.mounts_released - assert fake.container is None - assert await client.stop(second.lease) == stopped - - candidate = await client.start( - _request(workspace_id, "fixture:candidate:1", mode="candidate") - ) - cleaned = await client.cleanup_candidates(workspace_id) - assert tuple(item.lease for item in cleaned) == (candidate.lease,) - assert fake.container is None - finally: - task.cancel() - with pytest.raises(asyncio.CancelledError): - await task - - -@pytest.mark.asyncio -async def test_controller_adopts_docker_normalized_writable_mounts( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - workspace_id = server._workspace_id # pyright: ignore[reportPrivateUsage] - - await server._start( # pyright: ignore[reportPrivateUsage] - _request(workspace_id, "fixture:formal:1") - ) - assert fake.container is not None - host = fake.container["HostConfig"] - assert isinstance(host, dict) - mounts = host["Mounts"] - assert isinstance(mounts, list) - host["Mounts"] = [ - {key: value for key, value in mount.items() if key != "ReadOnly"} - for mount in mounts - if isinstance(mount, dict) - ] - - receipt = await server._start( # pyright: ignore[reportPrivateUsage] - _request(workspace_id, "fixture:formal:2") - ) - - assert receipt["adopted_from_generation"] == "fixture:formal:1" - assert receipt["lease"]["container_id"] == "container-1" # type: ignore[index] - assert fake.create_count == 1 - assert fake.delete_count == 0 - saved = next(iter(server._leases.values())) # pyright: ignore[reportPrivateUsage] - assert saved["generation_id"] == "fixture:formal:2" - - -@pytest.mark.asyncio -async def test_controller_replaces_exact_lease_when_declared_spec_changes( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - workspace_id = server._workspace_id # pyright: ignore[reportPrivateUsage] - - first = await server._start( # pyright: ignore[reportPrivateUsage] - _request(workspace_id, "fixture:formal:1") - ) - saved_owner = dict(server._leases) # pyright: ignore[reportPrivateUsage] - server._leases.clear() # pyright: ignore[reportPrivateUsage] - with pytest.raises(RuntimeError, match="spec/owner"): - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - workspace_id, - "fixture:unowned:2", - user_namespaces=True, - ) - ) - assert fake.create_count == 1 - assert fake.delete_count == 0 - server._leases.update(saved_owner) # pyright: ignore[reportPrivateUsage] - second = await server._start( # pyright: ignore[reportPrivateUsage] - _request( - workspace_id, - "fixture:formal:2", - user_namespaces=True, - ) - ) - - first_lease = first["lease"] - second_lease = second["lease"] - assert isinstance(first_lease, dict) and isinstance(second_lease, dict) - assert first_lease["spec_digest"] != second_lease["spec_digest"] - assert second["adopted_from_generation"] is None - assert fake.create_count == 2 - assert fake.delete_count == 1 - assert len(server._leases) == 1 # pyright: ignore[reportPrivateUsage] - saved = next(iter(server._leases.values())) # pyright: ignore[reportPrivateUsage] - assert saved["generation_id"] == "fixture:formal:2" - assert fake.create_body is not None - host = fake.create_body["HostConfig"] - assert isinstance(host, dict) - security = host["SecurityOpt"] - assert isinstance(security, list) and len(security) == 2 - - -@pytest.mark.asyncio -async def test_controller_removes_a_new_container_when_start_fails( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - fake.fail_next_start = True - server._engine = fake # pyright: ignore[reportPrivateUsage] - request = _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:failed:1", - ) - - with pytest.raises(RuntimeError, match="start failed"): - await server._start(request) # pyright: ignore[reportPrivateUsage] - - assert fake.container is None - assert server._leases == {} # pyright: ignore[reportPrivateUsage] - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("mode", "expected"), - ( - ("formal", {"8080/tcp": [{"HostIp": "127.0.0.1", "HostPort": "18080"}]}), - ("candidate", {}), - ), -) -async def test_controller_publishes_declared_loopback_only_for_formal( - tmp_path: Path, - mode: str, - expected: dict[str, object], -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - f"fixture:{mode}:1", - mode=mode, - loopback=True, - ) - ) - - assert fake.create_body is not None - host = fake.create_body["HostConfig"] - assert isinstance(host, dict) - assert host["PortBindings"] == expected - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "drift", - ( - "published_port", - "extra_network", - "extra_mount", - "host_pid", - "device", - "restart", - ), -) -async def test_controller_rejects_actual_container_config_drift( - tmp_path: Path, - drift: str, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - workspace_id = server._workspace_id # pyright: ignore[reportPrivateUsage] - await server._start( # pyright: ignore[reportPrivateUsage] - _request(workspace_id, "fixture:drift:1") - ) - assert fake.container is not None - if drift == "published_port": - host = fake.container["HostConfig"] - assert isinstance(host, dict) - host["PortBindings"] = {"8080/tcp": [{"HostPort": "8080"}]} - elif drift == "extra_network": - network_settings = fake.container["NetworkSettings"] - assert isinstance(network_settings, dict) - networks = network_settings["Networks"] - assert isinstance(networks, dict) - networks["extra"] = {} - elif drift == "extra_mount": - mounts = fake.container["Mounts"] - assert isinstance(mounts, list) - mounts.append( - { - "Type": "volume", - "Source": "/other", - "Destination": "/other", - "RW": True, - } - ) - else: - host = fake.container["HostConfig"] - assert isinstance(host, dict) - if drift == "host_pid": - host["PidMode"] = "host" - elif drift == "device": - host["Devices"] = [{"PathOnHost": "/dev/null"}] - else: - host["RestartPolicy"] = {"Name": "always", "MaximumRetryCount": 0} - - with pytest.raises(RuntimeError): - await server._start( # pyright: ignore[reportPrivateUsage] - _request(workspace_id, "fixture:drift:2") - ) - - -@pytest.mark.asyncio -async def test_controller_recovers_when_create_response_is_lost(tmp_path: Path) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - fake.lose_create_response = True - server._engine = fake # pyright: ignore[reportPrivateUsage] - - receipt = await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:lost-response:1", - ) - ) - - assert receipt["lease"]["container_id"] == "container-1" # type: ignore[index] - assert fake._running() # pyright: ignore[reportPrivateUsage] - - -@pytest.mark.asyncio -async def test_stop_keeps_mount_evidence_across_a_crash_after_delete( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - state_path = tmp_path / "state" / "leases.json" - arguments = { - "workspace": workspace, - "socket_path": tmp_path / "run" / "controller.sock", - "docker_socket": tmp_path / "docker.sock", - "state_path": state_path, - "network": "test-network", - "allowed_uid": os.getuid(), - "socket_gid": os.getgid(), - "workload_uid": os.getuid(), - "workload_gid": os.getgid(), - "socket_uid": os.getuid(), - } - server = WorkloadControllerServer(**arguments) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - request = _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:crash:1", - ) - await server._start(request) # pyright: ignore[reportPrivateUsage] - lease = WorkloadLease( - workspace_id=request.workspace_id, - plugin_id=request.plugin_id, - workload=request.workload, - mode=request.mode, - transaction_id=request.transaction_id, - generation_id=request.generation_id, - container_id="container-1", - spec_digest=request.spec_digest, - ) - fake.crash_after_delete = True - - with pytest.raises(SystemExit, match="after delete"): - await server._stop(lease) # pyright: ignore[reportPrivateUsage] - - recovered = WorkloadControllerServer(**arguments) - recovered._engine = fake # pyright: ignore[reportPrivateUsage] - receipt = await recovered._stop(lease) # pyright: ignore[reportPrivateUsage] - evidence = next( - iter(recovered._stopped.values()) - ) # pyright: ignore[reportPrivateUsage] - assert evidence["sources"] - assert receipt["container_absent"] is True - assert receipt["mounts_released"] is True - - -@pytest.mark.asyncio -async def test_completed_stop_receipts_are_not_silently_removed(tmp_path: Path) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - request = _request( - server._workspace_id, # pyright: ignore[reportPrivateUsage] - "fixture:retention:1", - ) - receipt = await server._start(request) # pyright: ignore[reportPrivateUsage] - server._stopped.update( # pyright: ignore[reportPrivateUsage] - { - f"old-{index}": {"lease": {}, "sources": [], "complete": True} - for index in range(1024) - } - ) - - await server._stop( # pyright: ignore[reportPrivateUsage] - WorkloadLease(**receipt["lease"]) # type: ignore[arg-type] - ) - - assert "old-0" in server._stopped # pyright: ignore[reportPrivateUsage] - assert len(server._stopped) == 1025 # pyright: ignore[reportPrivateUsage] - - -@pytest.mark.asyncio -async def test_controller_stops_workloads_when_its_core_container_stops( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - owner_container="akashic-core", - ) - fake = _FakeEngine() - fake.owner_running = True - server._engine = fake # pyright: ignore[reportPrivateUsage] - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, "fixture:owner-stop:1" - ) # pyright: ignore[reportPrivateUsage] - ) - - await server._check_owner(1.0) # pyright: ignore[reportPrivateUsage] - assert fake.container is not None - fake.owner_running = False - await server._check_owner(2.0) # pyright: ignore[reportPrivateUsage] - - assert fake.container is None - assert not server._leases # pyright: ignore[reportPrivateUsage] - assert (workspace / "plugin-data/fixture-builtin/state").is_dir() - - -@pytest.mark.asyncio -async def test_owner_cleanup_rechecks_core_after_waiting_for_request_lock( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - owner_container="akashic-core", - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, "fixture:owner-restart:1" - ) # pyright: ignore[reportPrivateUsage] - ) - server._owner_seen_running = True # pyright: ignore[reportPrivateUsage] - first_inspect_done = asyncio.Event() - inspection = 0 - - async def inspect_owner( - _container: str, *, allow_missing: bool = False - ) -> dict[str, object] | None: - nonlocal inspection - assert allow_missing - inspection += 1 - if inspection == 1: - first_inspect_done.set() - return {"State": {"Running": False}} - return {"State": {"Running": True}} - - server._inspect = inspect_owner # type: ignore[method-assign] # pyright: ignore[reportPrivateUsage] - await server._lock.acquire() # pyright: ignore[reportPrivateUsage] - cleanup = asyncio.create_task( - server._check_owner(2.0) # pyright: ignore[reportPrivateUsage] - ) - await first_inspect_done.wait() - server._lock.release() # pyright: ignore[reportPrivateUsage] - await cleanup - - assert inspection == 2 - assert fake.container is not None - assert server._leases # pyright: ignore[reportPrivateUsage] - - -@pytest.mark.asyncio -async def test_owner_cleanup_retries_the_same_exact_lease(tmp_path: Path) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - owner_container="akashic-core", - ) - fake = _FakeEngine() - fake.owner_running = True - server._engine = fake # pyright: ignore[reportPrivateUsage] - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, "fixture:owner-retry:1" - ) # pyright: ignore[reportPrivateUsage] - ) - await server._check_owner(1.0) # pyright: ignore[reportPrivateUsage] - - fake.owner_running = False - fake.fail_next_stop = True - with pytest.raises(ExceptionGroup, match="owner cleanup"): - await server._check_owner(2.0) # pyright: ignore[reportPrivateUsage] - assert fake.container is not None - - await server._check_owner(3.0) # pyright: ignore[reportPrivateUsage] - assert fake.container is None - assert not server._leases # pyright: ignore[reportPrivateUsage] - - -@pytest.mark.asyncio -async def test_controller_cleans_old_leases_if_core_never_starts( - tmp_path: Path, -) -> None: - workspace = _workspace(tmp_path) - server = WorkloadControllerServer( - workspace=workspace, - socket_path=tmp_path / "run" / "controller.sock", - docker_socket=tmp_path / "docker.sock", - state_path=tmp_path / "state" / "leases.json", - network="test-network", - allowed_uid=os.getuid(), - socket_gid=os.getgid(), - workload_uid=os.getuid(), - workload_gid=os.getgid(), - socket_uid=os.getuid(), - owner_container="akashic-core", - owner_grace_seconds=5.0, - ) - fake = _FakeEngine() - server._engine = fake # pyright: ignore[reportPrivateUsage] - await server._start( # pyright: ignore[reportPrivateUsage] - _request( - server._workspace_id, "fixture:owner-missing:1" - ) # pyright: ignore[reportPrivateUsage] - ) - - await server._check_owner(10.0) # pyright: ignore[reportPrivateUsage] - await server._check_owner(14.9) # pyright: ignore[reportPrivateUsage] - assert fake.container is not None - await server._check_owner(15.0) # pyright: ignore[reportPrivateUsage] - - assert fake.container is None - assert not server._leases # pyright: ignore[reportPrivateUsage] - - -def test_controller_state_save_syncs_file_and_directory( - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, -) -> None: - synced: list[int] = [] - monkeypatch.setattr(os, "fsync", synced.append) - - path = tmp_path / "state" / "leases.json" - WorkloadControllerServer._save_state( # pyright: ignore[reportPrivateUsage] - path, - {"one": {"complete": True}}, - ) - - assert path.read_text(encoding="utf-8") == '{"one":{"complete":true}}' - assert len(synced) == 2 diff --git a/tests/test_workspace_mcp_removed.py b/tests/test_workspace_mcp_removed.py deleted file mode 100644 index 9dbfdb323..000000000 --- a/tests/test_workspace_mcp_removed.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import annotations - -import importlib.util -from pathlib import Path - -from agent.plugins.manager import PluginManager - - -def test_workspace_mcp_legacy_modules_are_physically_unreachable() -> None: - for module_name in ( - "agent.mcp.declarations", - "agent.mcp.watcher", - "agent.mcp.admin", - "agent.mcp.generation", - "agent.tools.workspace_mcp", - ): - assert importlib.util.find_spec(module_name) is None - - -def test_workspace_mcp_manager_owner_and_builtin_skill_are_removed() -> None: - for name in ( - "active_workspace_mcp", - "prepared_workspace_mcp", - "prepare_workspace_mcp", - "publish_workspace_mcp", - "discard_workspace_mcp_candidate", - ): - assert not hasattr(PluginManager, name) - assert not ( - Path(__file__).parents[1] / "skills/manage-workspace-mcp" / "SKILL.md" - ).exists() - - -def test_workspace_init_does_not_create_removed_mcp_directories() -> None: - source = (Path(__file__).parents[1] / "bootstrap/init_workspace.py").read_text( - encoding="utf-8" - ) - - assert '"mcp"' not in source - assert '"mcp/servers"' not in source diff --git a/tests/turns/test_outbound.py b/tests/turns/test_outbound.py deleted file mode 100644 index cb6a5975a..000000000 --- a/tests/turns/test_outbound.py +++ /dev/null @@ -1,137 +0,0 @@ -from __future__ import annotations - -import asyncio - -import pytest - -from agent.plugin_composition.channels import ( - ChannelDeliveryReceipt, - DeliveryStatus as ChannelDeliveryStatus, -) -from agent.turns.outbound import OutboundDispatch, PushToolOutboundPort -from agent.tools.message_push import MessagePushTool -from bus.events import ChannelMessage, TurnTerminalStatus -from bus.queue import ChatLane - - -@pytest.mark.asyncio -async def test_push_tool_outbound_port_forwards_turn_identity_verbatim() -> None: - delivered: list[ChannelMessage] = [] - - class _PushTool: - async def dispatch( - self, - message: ChannelMessage, - *, - commit_role: str = "", - ) -> ChannelDeliveryReceipt: - delivered.append(message) - assert commit_role == "" - return ChannelDeliveryReceipt( - delivery_id="delivery-1", - status=ChannelDeliveryStatus.DELIVERED, - ) - - port = PushToolOutboundPort(_PushTool()) - - _ = await port.dispatch( - OutboundDispatch( - channel="telegram", - chat_id="123", - content="hello", - reply_to="message-1", - metadata={"source": "passive"}, - media=["/tmp/image.png"], - session_message_id="telegram:123:5", - control_turn_id="interaction:authoritative", - execution_attempt_id="turn:attempt", - terminal_status=TurnTerminalStatus.COMPLETED, - ) - ) - - assert len(delivered) == 1 - message = delivered[0] - assert message.control_turn_id == "interaction:authoritative" - assert message.execution_attempt_id == "turn:attempt" - assert message.terminal_status is TurnTerminalStatus.COMPLETED - assert message.reply_to == "message-1" - assert message.session_message_id == "telegram:123:5" - assert message.metadata == {"source": "passive"} - assert message.content == "hello" - - -@pytest.mark.asyncio -async def test_message_push_assigns_one_independent_turn_per_dispatch() -> None: - delivered: list[ChannelMessage] = [] - push = MessagePushTool() - - async def deliver( - message: ChannelMessage, - _passive: bool, - ) -> ChannelDeliveryReceipt: - delivered.append(message) - return ChannelDeliveryReceipt( - delivery_id=f"delivery-{len(delivered)}", - status=ChannelDeliveryStatus.DELIVERED, - ) - - push.bind_v3_channel_dispatcher(deliver) - for content in ("one", "two"): - receipt = await push.dispatch(ChannelMessage("mobile", "chat", content)) - assert receipt.status is ChannelDeliveryStatus.DELIVERED - - turn_ids = [message.control_turn_id for message in delivered] - assert all( - turn_id is not None and turn_id.startswith("turn:") - for turn_id in turn_ids - ) - assert len(set(turn_ids)) == 2 - - -@pytest.mark.asyncio -async def test_passive_outbound_uses_passive_commit_role_without_self_wait() -> None: - lane = ChatLane() - delivered: list[tuple[ChannelMessage, str]] = [] - push = MessagePushTool(chat_lane=lane) - - async def deliver( - message: ChannelMessage, - passive: bool, - ) -> ChannelDeliveryReceipt: - assert passive is True - - async def send() -> None: - delivered.append((message, "send")) - - await lane.run_passive(message.channel, message.chat_id, send) - return ChannelDeliveryReceipt( - delivery_id="delivery-passive", - status=ChannelDeliveryStatus.DELIVERED, - ) - - push.bind_v3_channel_dispatcher(deliver) - port = PushToolOutboundPort(push, commit_role="passive") - - await lane.mark_passive_pending("mobile", "chat") - try: - receipt = await asyncio.wait_for( - port.dispatch( - OutboundDispatch( - channel="mobile", - chat_id="chat", - content="passive reply", - reply_to="message-1", - session_message_id="mobile:chat:2", - control_turn_id="turn:passive", - media=["/tmp/image.png"], - ) - ), - timeout=0.5, - ) - finally: - await lane.mark_passive_done("mobile", "chat") - - assert receipt.status is ChannelDeliveryStatus.DELIVERED - assert delivered[0][0].reply_to == "message-1" - assert delivered[0][0].session_message_id == "mobile:chat:2" - assert delivered[0][0].control_turn_id == "turn:passive" diff --git a/tests_scenarios/contracts/coverage-baseline.json b/tests_scenarios/contracts/coverage-baseline.json index 41f64ff45..5fbd520de 100644 --- a/tests_scenarios/contracts/coverage-baseline.json +++ b/tests_scenarios/contracts/coverage-baseline.json @@ -1,7 +1,7 @@ { "acceptedGaps": [], "base": "683b4791b0c25e3e899d65fe7c028514240414b3", - "catalogDigest": "7cb0c976b1628a12e6650b79b2728b16fc19c52ecd055fe8156ae8a51536f962", + "catalogDigest": "d836dca54bbc07af1071f6c736b1d143ea808b9324e13617cfebcbe0994929f2", "purpose": "approved_contract_mapping", "coveredP0": { "companion_control": [ diff --git a/tests_scenarios/contracts/impact.toml b/tests_scenarios/contracts/impact.toml index 7a3ad66da..2a9b07c80 100644 --- a/tests_scenarios/contracts/impact.toml +++ b/tests_scenarios/contracts/impact.toml @@ -70,7 +70,6 @@ paths = [ "schema/archive/mobile-realtime-v1-mobile-pr6.json", "scripts/generate_mobile_realtime_schema.py", "tests/mobile_realtime/**", - "tests/test_plugin_mobile_ui.py", "tests_scenarios/mobile_isolated_gateway.py", ] depends_on = ["channel", "plugin", "session_persistence"] @@ -105,7 +104,6 @@ paths = [ "schema/mobile-realtime-v1.json", "scripts/generate_mobile_realtime_schema.py", "scripts/publish-mobile-webui.py", - "tests/mobile_realtime/test_mobile_realtime_protocol.py", "tests/mobile_webui/**", ] depends_on = ["mobile_realtime"] @@ -117,7 +115,6 @@ requirements = ["PLG-004", "PLG-008", "PLG-009", "TST-001", "TST-003"] paths = [ "agent/mcp/**", "bootstrap/toolsets/protocol.py", - "tests/test_workspace_mcp_removed.py", ] depends_on = ["plugin", "lifecycle"] scenarios = ["mcp_call_finality", "mcp_process_lifecycle"] @@ -139,8 +136,6 @@ paths = [ "bootstrap/tools.py", "bootstrap/wiring.py", "tests/test_plugin_*.py", - "tests/test_computer_plugin.py", - "tests/test_workload_controller.py", ] deleted_paths = [ "agent/tool_hooks/**", @@ -187,10 +182,7 @@ paths = [ "migrations/yoyo/*model*.py", "tests/model_plugin_fakes.py", "tests/test_*model*.py", - "tests/test_vision_tool.py", - "tests/test_akasha_plugin.py", "tests/test_plugin_generation_job_host.py", - "tests/test_web_shell.py", "tests/semantic/test_model_owner_contract.py", ] deleted_paths = ["agent/provider.py", "bootstrap/providers.py"] @@ -215,12 +207,8 @@ paths = [ "skills/develop-akashic-plugin/**", "tests/control/**", "tests/semantic/test_recursive_plugin_self_validation_contract.py", - "tests/semantic/test_recursive_plugin_self_validation_trajectory.py", - "tests/test_builtin_develop_akashic_plugin_skill.py", "tests/test_plugin_hot_reload.py", "tests/test_plugin_runtime_control.py", - "tests/test_support_modules.py", - "tests/test_turn_pipelines.py", ] depends_on = ["plugin", "control", "session_persistence", "channel", "memory"] scenarios = ["recursive_plugin_self_validation_contract"] @@ -253,9 +241,7 @@ paths = [ "plugins/eventmail/**", "plugins/wake/**", "plugins/drift/**", - "tests/test_content_*.py", "tests/test_wake_*.py", - "tests/test_drift_*.py", ] depends_on = ["events", "plugin"] scenarios = ["content_wake_delivery_contract"] @@ -375,7 +361,6 @@ paths = [ "scripts/prepare_container_rehearsal.py", "scripts/prepare_runtime_checkout.py", "scripts/verify_host_runtime_deployment.py", - "tests/test_akashic_release_installer.py", ] depends_on = ["runtime", "shell_finality", "session_persistence"] scenarios = ["host_bridge_boot_fencing_contract", "shell_privileged_cleanup_contract"] @@ -387,7 +372,6 @@ paths = [ "agent/migrations/**", "migrations/**", "scripts/check_yoyo_migrations.py", - "tests/test_main_lightweight_commands.py", "tests/test_migration_runner.py", "tests/test_yoyo_migration_append_only.py", ] @@ -412,7 +396,7 @@ scenarios = ["companion_tool_context_contract"] [groups.companion_external_io] priority = "p0" requirements = ["SEC-002", "FS-001", "SES-006", "ERR-001", "TST-001", "TST-003"] -paths = ["core/net/http.py", "agent/tools/web_fetch.py", "bootstrap/chat_api.py", "infra/channels/base.py", "infra/channels/delivery.py", "infra/channels/qq_channel.py", "infra/channels/telegram_channel.py", "infra/channels/web_chat_channel.py", "infra/mobile_realtime/attachments.py", "infra/mobile_realtime/channel.py", "infra/mobile_realtime/gateway.py", "infra/mobile_realtime/storage.py", "tests/mobile_realtime/test_attachments.py", "tests/mobile_realtime/test_channel.py", "tests/mobile_realtime/test_gateway.py", "tests/mobile_realtime/test_storage.py"] +paths = ["core/net/http.py", "agent/tools/web_fetch.py", "bootstrap/chat_api.py", "infra/channels/base.py", "infra/channels/delivery.py", "infra/channels/qq_channel.py", "infra/channels/telegram_channel.py", "infra/channels/web_chat_channel.py", "infra/mobile_realtime/attachments.py", "infra/mobile_realtime/channel.py", "infra/mobile_realtime/gateway.py", "infra/mobile_realtime/storage.py", "tests/mobile_realtime/test_attachments.py", "tests/mobile_realtime/test_channel.py", "tests/mobile_realtime/test_storage.py"] depends_on = ["runtime", "mobile_realtime"] scenarios = ["companion_external_io_contract"] diff --git a/tests_scenarios/contracts/retained-test-files.txt b/tests_scenarios/contracts/retained-test-files.txt new file mode 100644 index 000000000..71d4cbc10 --- /dev/null +++ b/tests_scenarios/contracts/retained-test-files.txt @@ -0,0 +1,72 @@ +tests/control/test_control_execution.py +tests/control/test_channel_adapter.py +tests/control/test_conversation_runtime.py +tests/control/test_d8_control_admission_replay.py +tests/control/test_protocol.py +tests/control/test_scoped_turn.py +tests/control/test_socket_security.py +tests/mobile_realtime/test_attachments.py +tests/mobile_realtime/test_channel.py +tests/mobile_realtime/test_key_protection.py +tests/mobile_realtime/test_remote_media.py +tests/mobile_realtime/test_storage.py +tests/mobile_webui/test_publication.py +tests/semantic/test_change_gate.py +tests/semantic/test_companion_contract.py +tests/semantic/test_context_history_contract.py +tests/semantic/test_contract_oracles.py +tests/semantic/test_model_owner_contract.py +tests/semantic/test_recursive_plugin_self_validation_contract.py +tests/test_activate_session_compaction_cursor_migration.py +tests/test_add_wake_content_scores_migration.py +tests/test_adopt_legacy_plugin_skill_links.py +tests/test_agent_restart.py +tests/test_akasha_embedding_backfill.py +tests/test_backfill_akasha_message_embeddings_migration.py +tests/test_backfill_explicit_programmatic_effects_migration.py +tests/test_backfill_plugin_programmatic_effects_migration.py +tests/test_context_compaction_contract.py +tests/test_dashboard_api.py +tests/test_host_bridge.py +tests/test_http_migrations.py +tests/test_job_store.py +tests/test_channel_attachment_store.py +tests/test_durable_deliveries.py +tests/test_mcp_process_recovery.py +tests/test_message_bus_admission.py +tests/test_migrate_compaction_plugin_config_migration.py +tests/test_migrate_eventmail_state_migration.py +tests/test_migrate_legacy_mobile_client_ids.py +tests/test_migrate_plugin_data.py +tests/test_migrate_proactive_delivery_target_migration.py +tests/test_migrate_turn_effects_migration.py +tests/test_migration_runner.py +tests/test_normalize_session_timestamps_migration.py +tests/test_plugin_composition_lifecycle.py +tests/test_plugin_generation_job_host.py +tests/test_plugin_hot_reload.py +tests/test_plugin_install.py +tests/test_plugin_managed_process_host.py +tests/test_plugin_channel_credentials.py +tests/test_plugin_runtime_control.py +tests/test_plugin_turn_rollout.py +tests/test_proactive_feedback_emotion_interop.py +tests/test_remove_compaction_trigger_migration.py +tests/test_retire_core_model_config_migration.py +tests/test_retire_legacy_context_state_migration.py +tests/test_session_compaction_prepare_migration.py +tests/test_session_compaction_runtime.py +tests/test_session_compaction_source_plan_digest_migration.py +tests/test_session_context_compaction_migration.py +tests/test_session_mutation_audit_migration.py +tests/test_session_store.py +tests/test_shell_tool.py +tests/test_tool_executor.py +tests/test_unified_exec.py +tests/test_unify_akashic_channel_identity_migration.py +tests/test_wake_durable_delivery.py +tests/test_wake_gate_contract.py +tests/test_rolling_backup.py +tests/test_runtime_smoke.py +tests/test_web_chat_channel.py +tests/test_yoyo_migration_append_only.py diff --git a/tests_scenarios/contracts/scenarios.toml b/tests_scenarios/contracts/scenarios.toml index 53f693961..b7f450abd 100644 --- a/tests_scenarios/contracts/scenarios.toml +++ b/tests_scenarios/contracts/scenarios.toml @@ -53,7 +53,13 @@ requirements = ["WSP-001", "WSP-004", "TST-005"] groups = ["runtime", "tooling"] environment = "public_clean_workspace" timeout_seconds = 120 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_gate_workspace_contract.py"] +command = [ + "python", + "-m", + "pytest", + "-q", + "tests/semantic/test_contract_oracles.py::test_workspace_oracle_rejects_official_path_mutant", +] observes = ["workspace_path", "plugin_home_path", "fresh_config"] mutants = ["official_workspace_path"] @@ -132,9 +138,6 @@ command = [ "pytest", "-q", "tests/test_host_bridge.py", - "tests/test_akashic_release_installer.py", - "tests/test_runtime_identity.py", - "tests/test_host_runtime_healthcheck.py", ] observes = [ "release_identity", @@ -179,9 +182,6 @@ command = [ "pytest", "-q", "tests/mobile_realtime", - "tests/test_plugin_mobile_ui.py", - "tests/test_app_server.py", - "tests/test_channel_host.py", ] observes = ["protocol_frames", "session_identity", "durable_delivery", "resume_cursor", "attachment_finality", "plugin_ui_snapshot", "master_key_write_set", "master_key_file", "keyset_manifest", "restart_identity"] mutants = ["unicode_utf16_command_length"] @@ -214,7 +214,7 @@ command = [ "pytest", "-q", "tests/mobile_webui", - "tests/mobile_realtime/test_mobile_realtime_protocol.py", + "tests/mobile_realtime/test_channel.py", ] observes = [ "release_view", @@ -242,9 +242,7 @@ command = [ "-q", "--basetemp=/sandbox/pytest-mcp-call-finality", "tests/semantic/test_contract_oracles.py", - "tests/test_plugin_composition_mcp_slots.py::test_mcp_registry_freezes_descriptor_health_and_cleanup", - "tests/test_plugin_composition_mcp_slots.py::test_static_manifest_is_admission_source_and_reconciles_mcp_root", - "tests/test_workspace_mcp_removed.py", + "tests/test_mcp_process_recovery.py::test_mcp_client_call_gate_waits_for_recovery_and_disconnect_cancels_it", ] observes = ["call_return", "immediate_read_after_call"] mutants = ["async_accepted_before_visible"] @@ -259,8 +257,6 @@ command = [ "-m", "pytest", "-q", - "tests/test_io_modules.py::test_mcp_client_cleans_wrapper_process_group", - "tests/test_io_modules.py::test_mcp_client_retains_ownership_when_process_group_cleanup_fails", "tests/test_plugin_managed_process_host.py::test_formal_fixed_port_and_candidate_are_isolated", "tests/test_plugin_managed_process_host.py::test_process_exit_recovers_with_new_epoch_without_stale_resurrection", "tests/test_plugin_managed_process_host.py::test_cleanup_failure_retains_tombstone_until_retry", @@ -301,10 +297,6 @@ command = [ "-q", "tests/semantic/test_contract_oracles.py::test_plugin_drain_oracle_rejects_active_only_completion_mutant", "tests/semantic/test_contract_oracles.py::test_plugin_data_oracle_rejects_uninstall_delete_mutant", - "tests/test_plugin_workload_manifest.py", - "tests/test_plugin_workload_core.py", - "tests/test_workload_controller.py", - "tests/test_computer_plugin.py", ] observes = [ "workload_declaration", @@ -331,27 +323,10 @@ command = [ "pytest", "-q", "tests/semantic/test_model_owner_contract.py", - "tests/test_models_plugin_ordinary_install.py::test_models_plugin_installs_and_runs_without_builtin_source", - "tests/test_openai_compatible_model_plugin.py::test_driver_is_an_installable_ordinary_artifact", - "tests/test_codex_model_plugin.py::test_codex_is_an_ordinary_installed_plugin_with_login_refresh_and_continuation", - "tests/test_opencode_go_model_plugin.py::test_driver_is_an_installable_ordinary_artifact", - "tests/test_compaction_markdown_memory_e2e.py", - "tests/test_markdown_memory_plugin.py", - "tests/test_model_control_api.py", - "tests/test_turn_pipelines.py::test_runtime_admission_runs_normal_tool_loop_through_chat_models", - "tests/test_models_plugin_store.py::test_saved_model_service_rejects_another_runtime_snapshot", - "tests/test_models_plugin_store.py::test_model_service_requires_owner_task_snapshot_lease", - "tests/test_models_plugin_store.py::test_model_capabilities_sources_and_driver_config_round_trip", - "tests/test_vision_tool.py", "tests/test_plugin_generation_job_host.py::test_llm_lease_is_invocation_scoped_and_invalid_after_handler", "tests/test_plugin_generation_job_host.py::test_model_retry_reuses_one_exact_binding_and_execution", "tests/test_plugin_generation_job_host.py::test_model_view_rejects_inherited_child_task", "tests/test_plugin_generation_job_host.py::test_queued_request_selects_model_generation_at_execution_start", - "tests/test_akasha_plugin.py::test_explicit_reindex_backs_up_and_publishes_descriptor_space", - "tests/test_akasha_plugin.py::test_engine_rejects_a_bound_embedding_space_change", - "tests/test_web_shell.py::test_model_control_crosses_public_shell_and_real_chat_socket", - "tests/test_web_chat_channel.py::test_chat_model_catalog_reports_session_override", - "tests/test_retire_core_model_config_migration.py", "tests/test_migration_runner.py", ] observes = [ @@ -415,13 +390,11 @@ command = [ "-m", "pytest", "-q", - "tests/test_plugin_composition_loader.py::test_v3_reload_keeps_old_root_until_snapshot_lease_drains", "tests/test_plugin_runtime_control.py::test_installed_mcp_update_keeps_old_artifact_until_lease_drains", "tests/test_plugin_turn_rollout.py::test_revert_is_same_turn_only_and_uninstall_stays_reversible", "tests/control/test_protocol.py::test_plugin_uninstall_returns_before_old_turn_drain_completes", "tests/control/test_protocol.py::test_plugin_uninstall_survives_deferred_client_disconnect", "tests/test_shell_tool.py::test_shell_env_exports_plugin_rollout_owner_turn", - "tests/test_runtime_smoke.py::test_plugin_uninstall_passes_active_turn_owner", "tests/test_plugin_install.py::test_plugin_enable_disable_and_uninstall_preserve_data", ] observes = [ @@ -448,7 +421,6 @@ command = [ "pytest", "-q", "tests/semantic/test_contract_oracles.py::test_memory_oracle_rejects_derived_state_overwrite_mutant", - "tests/test_akasha_plugin.py", "tests/test_session_store.py", ] observes = ["long_term_owner", "session_source"] @@ -465,27 +437,16 @@ command = [ "pytest", "-q", "tests/semantic/test_recursive_plugin_self_validation_contract.py", - "tests/semantic/test_recursive_plugin_self_validation_trajectory.py", "tests/test_plugin_runtime_control.py", - "tests/test_turn_pipelines.py::test_process_direct_runs_concurrently_with_another_session", - "tests/test_turn_pipelines.py::test_process_direct_waits_for_the_same_session_lane", + "tests/control/test_conversation_runtime.py::test_runtime_executes_different_threads_concurrently", + "tests/control/test_conversation_runtime.py::test_runtime_rejects_same_thread_input_and_interrupts_exact_turn", "tests/test_plugin_hot_reload.py::test_runtime_snapshot_latest_requires_explicit_selector_and_promotion", - "tests/test_plugin_composition_loader.py::test_candidate_uses_isolated_data_copy", "tests/test_plugin_hot_reload.py::test_startup_recovers_installed_candidate_from_durable_pointers", - "tests/test_plugin_composition_loader.py::test_installed_v3_candidate_health_blocks_promotion_until_recovered", "tests/test_plugin_hot_reload.py::test_passive_runtime_admission_holds_one_snapshot", - "tests/test_plugin_doctor.py::test_plugin_doctor_reads_latest_artifact_candidate", "tests/control/test_control_execution.py::test_control_execution_preserves_inbound_metadata", - "tests/control/test_exec_cli.py::test_exec_new_rejects_unbound_latest_and_defaults_to_stable", - "tests/control/test_exec_cli.py::test_control_client_reads_terminal_larger_than_asyncio_default", "tests/control/test_protocol.py::test_thread_runtime_selector_rejects_persisted_latest", "tests/control/test_protocol.py::test_router_disconnect_interrupts_only_attached_turn", - "tests/test_turn_effects.py", "tests/test_session_compaction_runtime.py::test_suppressed_turn_commit_advances_ledger_without_markdown", - "tests/test_support_modules.py::test_message_push_passive_role_is_forwarded_to_committed_dispatcher", - "tests/test_support_modules.py::test_message_push_passive_send_does_not_consume_queued_outbound_pending", - "tests/test_support_modules.py::test_context_builder_debug_projection_is_turn_local", - "tests/test_builtin_develop_akashic_plugin_skill.py", ] observes = [ "runtime_pointer_snapshot", @@ -516,7 +477,7 @@ requirements = ["SEC-001", "ERR-001", "TST-001", "TST-003"] groups = ["companion_tool_context"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_tool_context_rejects_origin_override_mutant", "tests/test_tool_executor.py", "tests/test_recall_memory_tool.py", "tests/turns/test_outbound.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_tool_context_rejects_origin_override_mutant", "tests/test_tool_executor.py"] observes = ["tool_execution_context", "schema_rejection", "outbound_target", "memory_provenance", "failure_semantics"] mutants = ["companion_tool_context_origin_override"] @@ -525,7 +486,7 @@ requirements = ["SEC-002", "FS-001", "SES-006", "ERR-001", "TST-001", "TST-003"] groups = ["companion_external_io"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_external_io_rejects_ownerless_spill_mutant", "tests/test_http_resources.py", "tests/test_http_migrations.py", "tests/test_channel_clients.py", "tests/mobile_realtime/test_attachments.py", "tests/mobile_realtime/test_channel.py", "tests/mobile_realtime/test_gateway.py", "tests/mobile_realtime/test_storage.py", "tests/test_web_chat_channel.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_external_io_rejects_ownerless_spill_mutant", "tests/mobile_realtime/test_attachments.py", "tests/mobile_realtime/test_channel.py", "tests/mobile_realtime/test_storage.py"] observes = ["redirect_hop_url", "stream_byte_count", "spill_owner", "spill_lifetime", "cleanup_diagnostic", "attachment_write_set", "logical_message_receipt", "target_device_commit", "failure_semantics"] mutants = ["companion_external_io_spill_owner"] @@ -534,7 +495,7 @@ requirements = ["SEC-003", "ERR-001", "TST-001", "TST-003"] groups = ["companion_peer_removal"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_peer_surface_removal_rejects_surviving_route_mutant", "tests/test_peer_agent_tool.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_peer_surface_removal_rejects_surviving_route_mutant"] observes = ["peer_config_schema", "tool_catalog", "route_registry", "prompt_surface", "legacy_config_error"] mutants = ["companion_peer_surface_survives"] @@ -543,7 +504,7 @@ requirements = ["OUT-001", "PRO-002", "TST-001", "TST-002", "TST-003"] groups = ["content_wake_drift"] environment = "public_clean_workspace" timeout_seconds = 90 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_content_delivery_rejects_early_source_ack_mutant", "tests/test_content_store.py", "tests/test_wake_durable_delivery.py", "tests/test_wake_gate_contract.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_content_delivery_rejects_early_source_ack_mutant", "tests/test_wake_durable_delivery.py", "tests/test_wake_gate_contract.py"] observes = ["content_write_set", "durable_delivery_receipt", "source_ack_receipt", "restart_reconciliation", "failure_semantics"] mutants = ["content_source_ack_precedes_delivery"] @@ -552,7 +513,7 @@ requirements = ["SEC-005", "SCH-001", "SCH-002", "ERR-001", "TST-001", "TST-003" groups = ["companion_schedule"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_schedule_capacity_rejects_unbounded_add_mutant", "tests/test_scheduler_v3_shadow.py", "tests/test_job_store.py", "tests/test_fire_at.py", "tests/test_time_parsing.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_schedule_capacity_rejects_unbounded_add_mutant", "tests/test_job_store.py"] observes = ["active_job_count", "candidate_commit", "schedule_capacity_error", "existing_jobs_unchanged", "failure_semantics"] mutants = ["companion_schedule_unbounded"] @@ -561,7 +522,7 @@ requirements = ["SEC-006", "MOB-003", "PLG-003", "PLG-004", "ERR-001", "TST-001" groups = ["companion_mobile"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_receipt_retention_rejects_valid_delete_mutant", "tests/mobile_realtime/test_storage.py", "tests/mobile_realtime/test_channel.py", "tests/mobile_realtime/test_gateway.py", "tests/test_message_bus_admission.py", "tests/test_plugin_mobile_ui.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_receipt_retention_rejects_valid_delete_mutant", "tests/mobile_realtime/test_storage.py", "tests/mobile_realtime/test_channel.py", "tests/test_message_bus_admission.py"] observes = ["receipt_retention", "receipt_high_water", "external_effect_reconciliation", "durable_inbound_handoff", "durable_recovery_page", "receipt_write_set", "query_lease", "generation_drain", "failure_semantics"] mutants = ["companion_receipt_deletes_valid_result"] @@ -570,7 +531,7 @@ requirements = ["SEC-007", "SH-001", "SH-002", "RUN-003", "ERR-001", "TST-001", groups = ["companion_shell"] environment = "public_clean_workspace" timeout_seconds = 60 -command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_shell_cleanup_rejects_turn_rewrite_mutant", "tests/test_shell_tool.py", "tests/test_subagent_v3_runtime.py", "tests/test_unified_exec.py"] +command = ["python", "-m", "pytest", "-q", "tests/semantic/test_companion_contract.py::test_shell_cleanup_rejects_turn_rewrite_mutant", "tests/test_shell_tool.py", "tests/test_unified_exec.py"] observes = ["execution_owner", "retained_log", "cleanup_diagnostic", "shared_admission", "turn_finality", "failure_semantics"] mutants = ["companion_shell_cleanup_rewrites_turn"]