Add real execution agent probe harness
Why: verify configured Agent CLIs one profile at a time before declaring them usable Evidence: 53 focused tests, agent-exec validation, 397-test governance regression, and diff check Impact: adds opt-in source-external probes, acceptance checks, and current task records Refs: U5; docs/superpowers/plans/2026-10-15-project-agent-execution.md
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
|
||||
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
|
||||
- 人工决策点:仅目标/范围/验收变更、未知外部副作用、高后果操作或无法自动判定的业务验收;计划完成后不停顿
|
||||
- 当前阶段:实施与自检;当前增量:U4(项目治理接线;U1–U3 已合入规范基线)
|
||||
- 当前阶段:实施与自检;当前增量:U5(opt-in 真实 CLI 探针夹具;U4 已规范合入 `0f4c5c017d444369470443faede3283e101c3be9`;七个 profile 探针 NOT_RUN)
|
||||
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
|
||||
|
||||
## 目标与非目标
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## Agent 实施收据
|
||||
|
||||
现行执行权威:根目录 [AGENT-EXECUTION.md](../AGENT-EXECUTION.md) 的 agent-per-unit 路由与收据门控的 host fallback(`host-fallback-authorized`)。历史 I1/P2 Cursor 行仅为早期 cursor-per-unit 时期记录,不是现行权威,仓库检索不得将其当作活跃固定 Cursor 要求。U1–U3 早于活跃 agent-exec 路由,由 ce-work 固定 Cursor 控制器实施,非本配置角色路由选型。源码外 ce-work 运行根:`/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015`(`manifest.json`)。
|
||||
现行执行权威:根目录 [AGENT-EXECUTION.md](../AGENT-EXECUTION.md) 的 agent-per-unit 路由与收据门控的 host fallback(`host-fallback-authorized`)。历史 I1/P2 Cursor 行仅为早期 cursor-per-unit 时期记录,不是现行权威,仓库检索不得将其当作活跃固定 Cursor 要求。U1–U3 早于活跃 agent-exec 路由,由 ce-work 固定 Cursor 控制器实施,非本配置角色路由选型。源码外 ce-work 运行根:`/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015`(`manifest.json`)。U4 已由 host 集成:规范 `0f4c5c017d444369470443faede3283e101c3be9`(父 `8f11d002aec74ed21d3dae8ccbb760963b99e93a`,树 `687ac553730e12e67e6ff3540162a217b8e46e05`),accepted `attempt-2`,job `20261015T045650Z-ab6d6c41`,transport `2ef4f76887d5dfc8a069c74da03f32a8be12e88a`,`model_receipt_status=verified`(Cursor Agent / Auto);attempt-1 已放弃。以上为该 manifest 的源码外事实,本卡不手抄命令输出。U5 七个 profile 探针保持 NOT_RUN。
|
||||
|
||||
| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
@@ -33,8 +33,23 @@
|
||||
| U1 | 配置合同 AGENT-EXECUTION.md / parser | 早于活跃 agent-exec 路由;ce-work 固定 Cursor 控制器;accepted `attempt-4`;job `20261015T021049Z-c63f0c9c`;见 `manifest.json` | implementation | 非角色路由;ce-work 固定 Cursor | attempts 1–4;accepted `attempt-4` | Cursor Agent / Auto(已核实) | 完成;规范 `bdad8ffc91beab7f0008a3382b650bfa15394065` | `5ce1c685ccf9b4da20f0ca7bb2449bf0ce8cef45` | 无 / 未授权 | `/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015`(manifest.json) |
|
||||
| U2 | adapter seam | 早于活跃 agent-exec 路由;ce-work 固定 Cursor 控制器;accepted `attempt-4`;job `20261015T025219Z-8edd087e`;见 `manifest.json` | implementation | 非角色路由;ce-work 固定 Cursor | attempts 1–4;accepted `attempt-4` | Cursor Agent / Auto(已核实) | 完成;规范 `92330a225f9d839aaaf279e6ae1ad96a4bdfebb4` | `cfdae5f9cf0dbec3ad991df77d39852236927255` | 无 / 未授权 | `/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015`(manifest.json) |
|
||||
| U3 | 可恢复执行控制器 | 早于活跃 agent-exec 路由;ce-work 固定 Cursor 控制器;accepted `attempt-9`;job `20261015T042945Z-e824725f`;见 `manifest.json` | implementation | 非角色路由;ce-work 固定 Cursor | attempts 1–9;accepted `attempt-9` | Cursor Agent / Auto(已核实) | 完成;规范 `8f11d002aec74ed21d3dae8ccbb760963b99e93a` | `6671fc2d6bdb7b64d8a83cc53a27d9443c98e61a` | 无 / 未授权 | `/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015`(manifest.json) |
|
||||
| U4 | 项目治理接线(本单元) | NOT_RUN | implementation | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | `/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015` |
|
||||
| U5 | 真实 CLI 探针与组合验收 | NOT_RUN | implementation | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | — |
|
||||
| U4 | 项目治理接线 | ce-work 固定 Cursor 控制器;accepted `attempt-2`;job `20261015T045650Z-ab6d6c41`;attempt-1 已放弃;见源码外 `manifest.json` | implementation | 非角色路由;ce-work 固定 Cursor | attempts 1–2;accepted `attempt-2` | Cursor Agent / Auto(manifest `model_receipt_status=verified`) | 完成;规范 `0f4c5c017d444369470443faede3283e101c3be9` | `2ef4f76887d5dfc8a069c74da03f32a8be12e88a` | 无 / 未授权 | `/tmp/compound-engineering-1000/ce-work/project-agent-exec-20261015` |
|
||||
| U5 | 真实 CLI 探针与组合验收 | 夹具与验收说明已写入;七个 profile 探针 NOT_RUN | implementation | 每次恰好一个 `AGENT_EXEC_REAL_PROFILE` | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | 源码外收据待 host 执行后产生;不入库 |
|
||||
|
||||
### U5 真实探针字段(host 稍后逐 profile 执行)
|
||||
|
||||
门禁:`RUN_REAL_AGENT_EXEC=1`,且 `AGENT_EXEC_REAL_PROFILE` 每次恰好一个 profile。默认测试不接触 provider。缺失或不兼容的 OpenCode 记 `unavailable`,不得记 PASS。验收说明见 `tests/fixtures/agent-exec/README.md`。
|
||||
|
||||
| profile | 探针 | 模型收据 | transport | 变更路径 | 清理/保留 | 规范检出 | 能力结论 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| cursor | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| antigravity | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| claude | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| codex | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| pi | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| kimi | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| opencode | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
|
||||
|
||||
## 关联方向与规划
|
||||
|
||||
@@ -58,7 +73,7 @@
|
||||
|
||||
- 方案引用:[2026-10-15 项目级执行 Agent 路由设计](../docs/superpowers/specs/2026-10-15-project-agent-execution-design.md)(已批准)
|
||||
- 实施计划引用:[2026-10-15 项目级执行 Agent 路由实施计划](../docs/superpowers/plans/2026-10-15-project-agent-execution.md)(用户明确免除计划复审)
|
||||
- 测试计划引用:U1–U3 本地 unittest / agent-exec validate 为 SELF;U4 聚焦任务卡模板与 validate;U5 真实 CLI 探针与完整治理回归(GATE 待 PR HEAD)
|
||||
- 测试计划引用:U1–U3 本地 unittest / agent-exec validate 为 SELF;U4 聚焦任务卡模板与 validate(SELF,源码外 manifest verification summary);U5 真实 CLI 探针默认 NOT_RUN,仅 `RUN_REAL_AGENT_EXEC=1` 且单 profile 时由 host 执行;完整治理回归与 GATE 待 PR HEAD
|
||||
|
||||
## 阶段评审
|
||||
|
||||
@@ -69,7 +84,7 @@
|
||||
| 需求表确认 | R1–R7 | 通过 | weichen | 2026-10-15 | 用户确认多 CLI 受控 adapter、`run-everything` 优先、主模型低消耗与全候选不可用后回退 |
|
||||
| 方案设计 | `docs/superpowers/specs/2026-10-15-project-agent-execution-design.md` | 通过 | weichen | 2026-10-15 | 用户明确“批准” |
|
||||
| 实施计划 | `docs/superpowers/plans/2026-10-15-project-agent-execution.md` | 授权连续执行 | weichen | 2026-10-15 | 用户明确“实施计划后不需要再让我审阅”,并指定 Cursor Agent 执行 |
|
||||
| 实施与自检 | U1 `bdad8ffc` / U2 `92330a22` / U3 `8f11d002`;U4/U5 进行中 | 进行中 | Codex host + 外部 worker | 2026-10-15 | U1–U3 规范合入;U4 治理接线;U5 探针 NOT_RUN;历史 P2/`8b7e8ca` 仅作旧对象线索 |
|
||||
| 实施与自检 | U1 `bdad8ffc` / U2 `92330a22` / U3 `8f11d002` / U4 `0f4c5c017d444369470443faede3283e101c3be9`;U5 探针 NOT_RUN | 进行中 | Codex host + 外部 worker | 2026-10-15 | U1–U4 规范合入;U5 夹具已写、真实探针 NOT_RUN;历史 P2/`8b7e8ca` 仅作旧对象线索 |
|
||||
| 正式测试与验收 | 待定(U1–U5 组合候选) | 待执行 | — | — | 旧 `cursor-per-unit` 候选 `cc03710` 已停止交付;不发明 GATE/REPLAY |
|
||||
| 代码 Review | 待定 | 未评审 | — | — | — |
|
||||
| 合入 | 待定 | 未评审 | — | — | — |
|
||||
@@ -103,16 +118,16 @@
|
||||
|
||||
## 结果与验证(完成后填写)
|
||||
|
||||
- 结果:旧 cursor-per-unit 候选已停止交付并保留为历史;U0 设计已批准;U1–U3 已合入规范基线(`bdad8ffc…` / `92330a22…` / `8f11d002…`);U4 将项目规则改为配置驱动 agent-per-unit;U5 真实探针 NOT_RUN。
|
||||
- 验证:U1–U3 本地 SELF 见各单元聚焦测试与 host 集成;U4 本地聚焦任务卡测试与 `agent_exec.py validate`(SELF);完整 `make test-governance` / PR CI(GATE)由 host 权威事务执行,本卡不手抄。历史 P2 digest `d16726eb13aba7f5` 仅作旧对象线索。
|
||||
- 影响与剩余项:U5 探针、独立 Review、精确 HEAD CI、合入与任务卡闭合仍 NOT_RUN。
|
||||
- 结果:旧 cursor-per-unit 候选已停止交付并保留为历史;U0 设计已批准;U1–U4 已合入规范基线(`bdad8ffc…` / `92330a22…` / `8f11d002…` / `0f4c5c017d444369470443faede3283e101c3be9`);U5 真实探针 NOT_RUN。
|
||||
- 验证:U1–U3 本地 SELF 见各单元聚焦测试与 host 集成;U4 本地聚焦任务卡测试、`agent_exec.py validate` 与 `make test-governance` 的结论见源码外 manifest verification summary(SELF,digest `8f277d49b61e57d97e39beb16d5a4cad702e7043d670094e3ebe0c9b1d26945f`),本卡不手抄该次输出。U5 默认测试不接触 provider,七个 profile 探针 NOT_RUN。完整 PR CI(GATE)由 host 权威事务执行。历史 P2 digest `d16726eb13aba7f5` 仅作旧对象线索。
|
||||
- 影响与剩余项:U5 七个 profile 探针、host 回退演练、独立 Review、精确 HEAD CI、合入与任务卡闭合仍 NOT_RUN。
|
||||
|
||||
## 规划进展与下一步
|
||||
|
||||
- 本次推进/剩余:U1–U3 已完成;U4 治理接线进行中;U5 组合验收待办。
|
||||
- 首选下一任务:U5 真实 CLI 探针与组合验收(新 agent-per-unit 进程)。
|
||||
- 推荐依据:遵循本次 goal-first 与不扩围原则;U4 完成后才具备日常任务路由权威。
|
||||
- 前置条件/待确认:U4 聚焦验证通过并由 host 权威集成;无待人工复审项。
|
||||
- 本次推进/剩余:U1–U4 已规范合入;U5 已添加 opt-in 探针夹具与验收说明,七个 profile 的真实探针仍 NOT_RUN。
|
||||
- 首选下一任务:host 按 profile 逐个执行 U5 真实探针(每次一个 `AGENT_EXEC_REAL_PROFILE`),再进入 Review 与精确 HEAD CI。
|
||||
- 推荐依据:U4 已提供 agent-per-unit 路由权威;探针未执行前不得把已安装 CLI 记为就位。
|
||||
- 前置条件/待确认:host 在源码外逐 profile 执行探针;无待人工复审项。
|
||||
- 预期产物/验收条件:R1–R10 有代码/测试/证据映射;无活跃固定 Cursor 权威;U5 后决定是否声明已安装 CLI 就位。
|
||||
- 重复任务核对:未发现需另建任务的重复范围。
|
||||
- 启动授权:U1–U5 已授权连续执行;U5 之外新任务仍须明确授权
|
||||
|
||||
+4
-2
@@ -5,8 +5,10 @@ authority for which Agent CLI profiles may implement, repair, test, or review
|
||||
bounded units. Human-readable notes may appear outside the machine block; the
|
||||
parser accepts exactly one `agent-execution` fenced block.
|
||||
|
||||
U1 delivers configuration parsing and validation only. Routing is not enabled
|
||||
until later units wire adapters and the execution controller.
|
||||
U3's execution controller and U4's project wiring are integrated. This file is
|
||||
the execution-routing authority for implementation, repair, test, and review.
|
||||
Installed CLI identity stays unproven until a host runs the U5 probes one
|
||||
profile at a time.
|
||||
|
||||
```agent-execution
|
||||
schema: agent-execution/v1
|
||||
|
||||
Vendored
+68
@@ -0,0 +1,68 @@
|
||||
# U5 真实 Agent CLI 探针
|
||||
|
||||
本夹具描述 host 稍后执行的 opt-in 探针。七个 profile 的真实结果都还是 `NOT_RUN`。这里不记录探针 `PASS`。
|
||||
|
||||
## 门禁
|
||||
|
||||
默认测试不设置下面两个变量,因此不会启动真实 CLI,也不会联系 provider。
|
||||
|
||||
- `RUN_REAL_AGENT_EXEC=1`:显式打开真实探针。其他值(包括 `true`)保持关闭。
|
||||
- `AGENT_EXEC_REAL_PROFILE`:每次恰好一个 profile。允许的名字是 `cursor`、`antigravity`、`claude`、`codex`、`pi`、`kimi`、`opencode`。空值、空白、逗号或空格列表都会拒绝,并且不会启动 CLI。
|
||||
|
||||
一次只跑一个 profile,不要对同一个目标并行开多个探针。
|
||||
|
||||
```bash
|
||||
RUN_REAL_AGENT_EXEC=1 AGENT_EXEC_REAL_PROFILE=cursor \
|
||||
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \
|
||||
tests.test_agent_exec.RealAgentCliProbeTests.test_opt_in_real_profile_probe -v
|
||||
```
|
||||
|
||||
把 `cursor` 换成要测的那一个 profile。建议顺序:`cursor`,然后 `antigravity`(可执行文件 `agy`),`claude`,`pi`,`codex`,`kimi`。`opencode` 单独跑。
|
||||
|
||||
## 夹具
|
||||
|
||||
探针在临时 Git 仓库里进行,不使用本仓的规范工作区。
|
||||
|
||||
- 分支是非默认分支 `feature/u5-real-probe`。
|
||||
- 配置里只启用被选中的那一个 profile,`model` 为 `auto`(省略模型参数,不把字符串 `auto` 传给 CLI)。
|
||||
- 单元只允许一个文件:`src/u5-marker.txt`。
|
||||
- 文件字节必须是 ASCII `U5-REAL-PROBE-MARKER` 再加一个换行 `0x0A`。
|
||||
|
||||
## 核对
|
||||
|
||||
Host 只在机械检查全部成立时把能力结论记为 `PASS`:
|
||||
|
||||
- transport 树和工作区里的标记内容与上面的字节完全一致。
|
||||
- transport 提交只有一个父提交,且该父提交就是夹具基线 HEAD。
|
||||
- `changed_paths` 只有 `src/u5-marker.txt`。
|
||||
- 成功时保留隔离工作区,`transport_created` 为真,`exact_restore` 为假。
|
||||
- 模型收据只记录实际看得到的状态。原始输出里没有 `model_receipt_status` 时记 `not-observable`,不得编造成 `verified`。观察到 `mismatch` 时结论不是 `PASS`。
|
||||
- 规范检出的 HEAD、分支、状态和索引保持不变,标记文件不得出现在规范检出里。
|
||||
|
||||
## 不可用与回退
|
||||
|
||||
缺失或不兼容的 OpenCode 必须记 `unavailable`,不得记 `PASS`。不兼容指本地 help 缺少 adapter 要求的能力标志;这种情况下不得把 CLI 当成已经跑通。
|
||||
|
||||
唯一候选耗尽时,控制器的运行级终态可以是 `host-fallback-authorized`。那是回退授权,不是探针通过。夹具不会代替 CLI 去写标记文件。
|
||||
|
||||
Provider 配额、认证失败,或进程启动后没有留下标记且规范检出仍然完好,同样记 `unavailable`,不得记 `PASS`。范围外改动、规范检出被改写、标记内容不符,记 `failed`。
|
||||
|
||||
## 源码外收据
|
||||
|
||||
真实 opt-in 运行会保留临时仓库,并把收据复制到 `/tmp/agent-exec-<uid>/u5-probes/`。原始 CLI 日志留在控制器的私有 run 目录。这些文件不进入 Git。
|
||||
|
||||
默认单元测试使用隔离 PATH 上的假 CLI,只验证上述核对逻辑。
|
||||
|
||||
## 仍为 NOT_RUN
|
||||
|
||||
| profile | 探针 | 模型收据 | transport | 变更路径 | 清理/保留 | 规范检出 | 能力结论 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| cursor | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| antigravity | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| claude | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| codex | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| pi | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| kimi | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
| opencode | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN | NOT_RUN |
|
||||
|
||||
Host 回退演练、完整治理回归、独立 Review、精确 HEAD CI 与合入也不在本夹具的已执行范围内。
|
||||
+697
-1
@@ -1,8 +1,14 @@
|
||||
"""End-to-end execution controller contract for agent-exec (U3)."""
|
||||
"""End-to-end execution controller contract for agent-exec (U3).
|
||||
|
||||
U5 adds an opt-in real CLI probe. Default discovery does not contact providers.
|
||||
Set RUN_REAL_AGENT_EXEC=1 and AGENT_EXEC_REAL_PROFILE to exactly one profile.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
@@ -1368,5 +1374,695 @@ class AgentExecControllerTests(unittest.TestCase):
|
||||
self._assert_canonical_untouched()
|
||||
|
||||
|
||||
# Opt-in real CLI probes (U5). Default unittest must not contact providers.
|
||||
U5_PROFILES = (
|
||||
"cursor",
|
||||
"antigravity",
|
||||
"claude",
|
||||
"codex",
|
||||
"pi",
|
||||
"kimi",
|
||||
"opencode",
|
||||
)
|
||||
U5_EXECUTABLE = {
|
||||
"cursor": "cursor-agent",
|
||||
"antigravity": "agy",
|
||||
"claude": "claude",
|
||||
"codex": "codex",
|
||||
"pi": "pi",
|
||||
"kimi": "kimi",
|
||||
"opencode": "opencode",
|
||||
}
|
||||
U5_BRANCH = "feature/u5-real-probe"
|
||||
U5_MARKER_REL = "src/u5-marker.txt"
|
||||
U5_MARKER_BODY = "U5-REAL-PROBE-MARKER\n"
|
||||
_MODEL_RECEIPT_RE = re.compile(
|
||||
r'"model_receipt_status"\s*:\s*"(verified|not-observable|mismatch|unavailable)"'
|
||||
)
|
||||
|
||||
|
||||
def real_probe_opt_in(env: os._Environ[str] | dict[str, str] | None = None) -> bool:
|
||||
"""True only for the explicit host gate. Other truthy strings stay closed."""
|
||||
source = os.environ if env is None else env
|
||||
return source.get("RUN_REAL_AGENT_EXEC") == "1"
|
||||
|
||||
|
||||
def require_single_real_profile(env: dict[str, str]) -> str:
|
||||
"""Select exactly one installed-CLI profile. Reject lists and unknown names."""
|
||||
raw = env.get("AGENT_EXEC_REAL_PROFILE", "")
|
||||
if not raw or raw.strip() != raw:
|
||||
raise ValueError(
|
||||
"AGENT_EXEC_REAL_PROFILE must name exactly one profile"
|
||||
)
|
||||
if any(sep in raw for sep in (",", " ", "\t", "\n", ";", ":")):
|
||||
raise ValueError(
|
||||
"AGENT_EXEC_REAL_PROFILE must name exactly one profile"
|
||||
)
|
||||
if raw not in U5_PROFILES:
|
||||
raise ValueError(f"unknown real probe profile: {raw}")
|
||||
return raw
|
||||
|
||||
|
||||
def _observe_model_receipt(attempt: dict[str, object]) -> str:
|
||||
"""Record a model receipt only when the raw output actually carries one."""
|
||||
chunks = [str(attempt.get("summary") or "")]
|
||||
raw_log = attempt.get("raw_log")
|
||||
if isinstance(raw_log, str) and raw_log:
|
||||
path = Path(raw_log)
|
||||
if path.is_file():
|
||||
chunks.append(path.read_text(encoding="utf-8", errors="replace"))
|
||||
found = _MODEL_RECEIPT_RE.findall("\n".join(chunks))
|
||||
if not found:
|
||||
return "not-observable"
|
||||
if "mismatch" in found:
|
||||
return "mismatch"
|
||||
return found[-1]
|
||||
|
||||
|
||||
def _probe_tool_bindir(extra: Path | None = None) -> tempfile.TemporaryDirectory[str]:
|
||||
"""PATH containing only git, python3, and an optional fake CLI directory."""
|
||||
tool_dir = tempfile.TemporaryDirectory(prefix="u5-probe-path-")
|
||||
bindir = Path(tool_dir.name)
|
||||
if extra is not None:
|
||||
for entry in extra.iterdir():
|
||||
target = bindir / entry.name
|
||||
if entry.is_file():
|
||||
target.symlink_to(entry.resolve())
|
||||
for tool in ("git", "python3"):
|
||||
found = shutil.which(tool)
|
||||
if not found:
|
||||
tool_dir.cleanup()
|
||||
raise RuntimeError(f"required tool missing from PATH: {tool}")
|
||||
link = bindir / tool
|
||||
if not link.exists():
|
||||
link.symlink_to(found)
|
||||
return tool_dir
|
||||
|
||||
|
||||
def _write_probe_double(
|
||||
bindir: Path,
|
||||
name: str,
|
||||
*,
|
||||
help_text: str,
|
||||
run_body: str,
|
||||
) -> None:
|
||||
path = bindir / name
|
||||
# printf is a shell builtin. The offline PATH has no cat/mkdir.
|
||||
help_lines = help_text.splitlines() or [""]
|
||||
help_printf = "".join(
|
||||
f" printf '%s\\n' {shlex.quote(line)}\n" for line in help_lines
|
||||
)
|
||||
path.write_text(
|
||||
"#!/bin/sh\n"
|
||||
'if [ "$1" = "--version" ] || [ "$1" = "-v" ] || [ "$1" = "-V" ]; then\n'
|
||||
' echo "0.0.1-probe"\n'
|
||||
" exit 0\n"
|
||||
"fi\n"
|
||||
'if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then\n'
|
||||
+ help_printf
|
||||
+ " exit 0\n"
|
||||
"fi\n"
|
||||
+ run_body,
|
||||
encoding="utf-8",
|
||||
)
|
||||
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
||||
|
||||
|
||||
def _cursor_help() -> str:
|
||||
from scripts.agent_exec_adapters import _REQUIRED_HELP_MARKERS
|
||||
|
||||
return "\n".join(_REQUIRED_HELP_MARKERS["cursor"]) + "\n"
|
||||
|
||||
|
||||
def _marker_run_body(*, body: str, model_receipt: str | None) -> str:
|
||||
payload: dict[str, object] = {
|
||||
"terminal_status": "completed",
|
||||
"summary": "wrote marker",
|
||||
"changed_files": [U5_MARKER_REL],
|
||||
"evidence": [],
|
||||
"scope_expansion": None,
|
||||
}
|
||||
if model_receipt is not None:
|
||||
payload["model_receipt_status"] = model_receipt
|
||||
encoded = json.dumps(payload, separators=(",", ":"))
|
||||
writer = (
|
||||
"from pathlib import Path; "
|
||||
f"p=Path({json.dumps(U5_MARKER_REL)}); "
|
||||
"p.parent.mkdir(parents=True); "
|
||||
f"p.write_text({json.dumps(body)}, encoding='utf-8')"
|
||||
)
|
||||
return (
|
||||
f"python3 -c {shlex.quote(writer)}\n"
|
||||
f"printf '%s\\n' {shlex.quote(encoded)}\n"
|
||||
"exit 0\n"
|
||||
)
|
||||
|
||||
|
||||
def _write_probe_unit(path: Path, timeout_seconds: int) -> None:
|
||||
packet = {
|
||||
"unit_id": "u5-real-probe",
|
||||
"goal": (
|
||||
"Create src/u5-marker.txt containing the ASCII bytes "
|
||||
"U5-REAL-PROBE-MARKER followed by one newline (0x0A). "
|
||||
"Do not create or modify any other path."
|
||||
),
|
||||
"allowed_paths": [U5_MARKER_REL],
|
||||
"verification": ["test -f src/u5-marker.txt"],
|
||||
"constraints": [
|
||||
"Write only src/u5-marker.txt",
|
||||
"Exact bytes are U5-REAL-PROBE-MARKER plus a single trailing newline",
|
||||
"Leave the change uncommitted",
|
||||
"Do not commit, push, open a PR, merge, or deploy",
|
||||
],
|
||||
"network": True,
|
||||
"external_cost": True,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
}
|
||||
path.write_text(json.dumps(packet, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _cleanup_probe_runtime(repo: Path, receipt: dict[str, object] | None) -> None:
|
||||
if not receipt:
|
||||
return
|
||||
run_dir = receipt.get("run_dir")
|
||||
attempts = receipt.get("attempts")
|
||||
if isinstance(attempts, list):
|
||||
for attempt in attempts:
|
||||
if not isinstance(attempt, dict):
|
||||
continue
|
||||
worktree = attempt.get("worktree")
|
||||
if isinstance(worktree, str) and worktree:
|
||||
_git(repo, "worktree", "remove", "--force", worktree, check=False)
|
||||
shutil.rmtree(worktree, ignore_errors=True)
|
||||
if isinstance(run_dir, str) and run_dir:
|
||||
shutil.rmtree(run_dir, ignore_errors=True)
|
||||
|
||||
|
||||
def execute_marker_probe(
|
||||
profile: str,
|
||||
*,
|
||||
offline_bindir: Path | None,
|
||||
timeout_seconds: int = 20,
|
||||
retain_fixture: bool = False,
|
||||
) -> dict[str, object]:
|
||||
"""Run one marker unit against a single profile.
|
||||
|
||||
Offline fixtures pass ``offline_bindir`` and never use the ambient PATH.
|
||||
A real profile requires RUN_REAL_AGENT_EXEC=1 and keeps its fixture.
|
||||
"""
|
||||
if profile not in U5_EXECUTABLE:
|
||||
raise ValueError(f"unknown real probe profile: {profile}")
|
||||
if offline_bindir is None and not real_probe_opt_in():
|
||||
raise RuntimeError(
|
||||
"real Agent CLI probe refused without RUN_REAL_AGENT_EXEC=1"
|
||||
)
|
||||
|
||||
tool_dir: tempfile.TemporaryDirectory[str] | None = None
|
||||
repo_dir: tempfile.TemporaryDirectory[str] | None = None
|
||||
repo: Path | None = None
|
||||
receipt: dict[str, object] | None = None
|
||||
try:
|
||||
if offline_bindir is None:
|
||||
probe_path = os.environ.get("PATH", "")
|
||||
else:
|
||||
tool_dir = _probe_tool_bindir(offline_bindir)
|
||||
probe_path = tool_dir.name
|
||||
if retain_fixture:
|
||||
repo_root = Path(tempfile.mkdtemp(prefix="u5-real-probe-"))
|
||||
else:
|
||||
repo_dir = tempfile.TemporaryDirectory(prefix="u5-real-probe-")
|
||||
repo_root = Path(repo_dir.name)
|
||||
repo = repo_root / "repo"
|
||||
repo.mkdir()
|
||||
receipts = repo_root / "receipts"
|
||||
receipts.mkdir()
|
||||
init = _git(repo, "init", "-b", U5_BRANCH, check=False)
|
||||
if init.returncode != 0:
|
||||
_git(repo, "init")
|
||||
_git(repo, "checkout", "-b", U5_BRANCH)
|
||||
_git(repo, "config", "user.email", "agent-exec@example.com")
|
||||
_git(repo, "config", "user.name", "Agent Exec")
|
||||
(repo / "README.md").write_text("fixture\n", encoding="utf-8")
|
||||
_git(repo, "add", "README.md")
|
||||
_git(repo, "commit", "-m", "init")
|
||||
branch = _git(repo, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
|
||||
if branch != U5_BRANCH:
|
||||
raise RuntimeError(
|
||||
f"probe fixture must use non-default branch {U5_BRANCH}, got {branch}"
|
||||
)
|
||||
_write_config(
|
||||
repo,
|
||||
profiles={
|
||||
profile: {
|
||||
"adapter": profile,
|
||||
"executable": U5_EXECUTABLE[profile],
|
||||
"model": "auto",
|
||||
}
|
||||
},
|
||||
role_order=[profile],
|
||||
timeout_seconds=timeout_seconds,
|
||||
summary_bytes=4096,
|
||||
raw_output_bytes=65536,
|
||||
)
|
||||
before = _canonical_snapshot(repo)
|
||||
unit = repo_root / "unit.json"
|
||||
_write_probe_unit(unit, timeout_seconds)
|
||||
receipt_path = receipts / "probe.json"
|
||||
env = os.environ.copy()
|
||||
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
||||
env["PATH"] = probe_path
|
||||
workspace = Path(__file__).resolve().parents[1]
|
||||
existing = env.get("PYTHONPATH", "")
|
||||
env["PYTHONPATH"] = (
|
||||
f"{workspace}{os.pathsep}{existing}" if existing else str(workspace)
|
||||
)
|
||||
proc = subprocess.run(
|
||||
[
|
||||
"python3",
|
||||
"-m",
|
||||
"scripts.agent_exec",
|
||||
"run",
|
||||
"--repo",
|
||||
str(repo),
|
||||
"--role",
|
||||
"implementation",
|
||||
"--unit",
|
||||
str(unit),
|
||||
"--receipt",
|
||||
str(receipt_path),
|
||||
],
|
||||
cwd=str(repo_root),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=env,
|
||||
timeout=timeout_seconds + 30,
|
||||
)
|
||||
receipt = (
|
||||
json.loads(receipt_path.read_text(encoding="utf-8"))
|
||||
if receipt_path.is_file()
|
||||
else None
|
||||
)
|
||||
outcome = _classify_marker_probe(
|
||||
profile=profile,
|
||||
repo=repo,
|
||||
before=before,
|
||||
receipt=receipt,
|
||||
exit_code=proc.returncode,
|
||||
stderr=proc.stderr,
|
||||
)
|
||||
outcome["fixture_repo"] = str(repo)
|
||||
if retain_fixture and receipt_path.is_file():
|
||||
durable = Path(f"/tmp/agent-exec-{os.getuid()}") / "u5-probes"
|
||||
durable.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
os.chmod(durable, 0o700)
|
||||
stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
|
||||
copied = durable / f"{profile}-{stamp}.json"
|
||||
shutil.copy2(receipt_path, copied)
|
||||
outcome["source_external_receipt"] = str(copied)
|
||||
return outcome
|
||||
finally:
|
||||
if not retain_fixture and repo is not None:
|
||||
_cleanup_probe_runtime(repo, receipt if isinstance(receipt, dict) else None)
|
||||
if tool_dir is not None:
|
||||
tool_dir.cleanup()
|
||||
if repo_dir is not None:
|
||||
repo_dir.cleanup()
|
||||
|
||||
|
||||
def _classify_marker_probe(
|
||||
*,
|
||||
profile: str,
|
||||
repo: Path,
|
||||
before: dict[str, str],
|
||||
receipt: dict[str, object] | None,
|
||||
exit_code: int,
|
||||
stderr: str,
|
||||
) -> dict[str, object]:
|
||||
base: dict[str, object] = {
|
||||
"profile": profile,
|
||||
"exit_code": exit_code,
|
||||
"canonical_preserved": _canonical_snapshot(repo) == before,
|
||||
"marker_in_canonical": (repo / U5_MARKER_REL).exists(),
|
||||
"configured_model": None,
|
||||
"model_receipt_status": "not-observable",
|
||||
"branch": None,
|
||||
"changed_paths": [],
|
||||
"transport_parent": None,
|
||||
"transport_ref": None,
|
||||
"attempt_status": None,
|
||||
"run_terminal": None,
|
||||
"cleanup": {},
|
||||
"host_fallback_authorizations": 0,
|
||||
"base_head": before["head"],
|
||||
}
|
||||
if not base["canonical_preserved"]:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "canonical checkout mutated"
|
||||
return base
|
||||
if base["marker_in_canonical"]:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "canonical checkout contains the probe marker"
|
||||
return base
|
||||
if not isinstance(receipt, dict):
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = f"missing receipt: {stderr[-500:]}"
|
||||
return base
|
||||
|
||||
base["run_terminal"] = receipt.get("terminal_status")
|
||||
base["host_fallback_authorizations"] = receipt.get(
|
||||
"host_fallback_authorizations", 0
|
||||
)
|
||||
summary = receipt.get("config_summary")
|
||||
profiles = summary.get("profiles") if isinstance(summary, dict) else None
|
||||
roles = summary.get("roles") if isinstance(summary, dict) else None
|
||||
if not isinstance(profiles, dict) or set(profiles) != {profile}:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "probe configured a profile other than the selection"
|
||||
return base
|
||||
selected = profiles[profile]
|
||||
if isinstance(selected, dict):
|
||||
base["configured_model"] = selected.get("model")
|
||||
implementation = roles.get("implementation") if isinstance(roles, dict) else None
|
||||
if implementation != [profile]:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "implementation role is not exactly the selected profile"
|
||||
return base
|
||||
recorded_base = receipt.get("base")
|
||||
if isinstance(recorded_base, dict):
|
||||
base["branch"] = recorded_base.get("branch")
|
||||
if base["branch"] in {"main", "master", "HEAD", None}:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = f"probe branch is not a non-default branch: {base['branch']}"
|
||||
return base
|
||||
|
||||
attempts = receipt.get("attempts")
|
||||
if not isinstance(attempts, list) or len(attempts) != 1:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "probe must record exactly one attempt"
|
||||
return base
|
||||
attempt = attempts[0]
|
||||
if not isinstance(attempt, dict) or attempt.get("profile") != profile:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "attempt profile does not match the selection"
|
||||
return base
|
||||
base["attempt_status"] = attempt.get("terminal_status")
|
||||
base["changed_paths"] = list(attempt.get("changed_paths") or [])
|
||||
base["transport_ref"] = attempt.get("transport_ref")
|
||||
cleanup = attempt.get("cleanup") if isinstance(attempt.get("cleanup"), dict) else {}
|
||||
base["cleanup"] = {
|
||||
"exact_restore": bool(cleanup.get("exact_restore")),
|
||||
"transport_created": bool(cleanup.get("transport_created")),
|
||||
"workspace_preserved": bool(attempt.get("workspace_preserved")),
|
||||
"skipped": bool(cleanup.get("skipped")),
|
||||
}
|
||||
base["model_receipt_status"] = _observe_model_receipt(attempt)
|
||||
reason = attempt.get("reason") or receipt.get("reason")
|
||||
|
||||
if attempt.get("terminal_status") == "unavailable":
|
||||
base["capability"] = "unavailable"
|
||||
base["reason"] = reason or "adapter unavailable"
|
||||
if base["transport_ref"] or base["cleanup"]["transport_created"]:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "unavailable probe still created transport"
|
||||
return base
|
||||
|
||||
if (
|
||||
receipt.get("terminal_status") == "success"
|
||||
and attempt.get("terminal_status") == "success"
|
||||
):
|
||||
return _classify_successful_marker(
|
||||
repo=repo,
|
||||
receipt=receipt,
|
||||
attempt=attempt,
|
||||
before=before,
|
||||
base=base,
|
||||
)
|
||||
|
||||
changed = list(attempt.get("changed_paths") or [])
|
||||
if (
|
||||
base["canonical_preserved"]
|
||||
and not base["marker_in_canonical"]
|
||||
and not attempt.get("transport_ref")
|
||||
and not changed
|
||||
and attempt.get("terminal_status")
|
||||
in {"failed-clean", "failed-unknown", "unavailable"}
|
||||
):
|
||||
base["capability"] = "unavailable"
|
||||
base["reason"] = reason or "probe produced no marker"
|
||||
return base
|
||||
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = reason or f"unclassified probe status {attempt.get('terminal_status')}"
|
||||
return base
|
||||
|
||||
|
||||
def _classify_successful_marker(
|
||||
*,
|
||||
repo: Path,
|
||||
receipt: dict[str, object],
|
||||
attempt: dict[str, object],
|
||||
before: dict[str, str],
|
||||
base: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
if base["model_receipt_status"] == "mismatch":
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "model receipt mismatch"
|
||||
return base
|
||||
if base["configured_model"] is not None:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "auto model must omit the configured model value"
|
||||
return base
|
||||
changed = list(attempt.get("changed_paths") or [])
|
||||
if changed != [U5_MARKER_REL]:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = f"changed paths are not exactly the marker: {changed}"
|
||||
return base
|
||||
cleanup = base["cleanup"]
|
||||
if not isinstance(cleanup, dict):
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "missing cleanup record"
|
||||
return base
|
||||
if cleanup.get("exact_restore") or not cleanup.get("transport_created"):
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "success must retain transport and skip exact restore"
|
||||
return base
|
||||
if not cleanup.get("workspace_preserved"):
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "success must retain the isolated workspace until host review"
|
||||
return base
|
||||
transport = receipt.get("transport")
|
||||
ref = attempt.get("transport_ref")
|
||||
if not isinstance(transport, dict) or not isinstance(ref, str) or not ref:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "success is missing a transport ref"
|
||||
return base
|
||||
if transport.get("ref") != ref:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "receipt transport ref does not match the attempt"
|
||||
return base
|
||||
commit = _git(repo, "rev-parse", ref).stdout.strip()
|
||||
parents = _git(repo, "rev-list", "--parents", "-n", "1", commit).stdout.split()
|
||||
if len(parents) != 2:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = f"transport commit is not base-parented: {parents}"
|
||||
return base
|
||||
parent = parents[1]
|
||||
base_head = before["head"]
|
||||
recorded = receipt.get("base")
|
||||
recorded_head = recorded.get("head") if isinstance(recorded, dict) else None
|
||||
if parent != base_head or transport.get("parent") != base_head or recorded_head != base_head:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "transport parent is not the fixture base HEAD"
|
||||
return base
|
||||
blob = _git(repo, "cat-file", "-p", f"{commit}:{U5_MARKER_REL}").stdout
|
||||
worktree = attempt.get("worktree")
|
||||
live = ""
|
||||
if isinstance(worktree, str):
|
||||
marker_path = Path(worktree) / U5_MARKER_REL
|
||||
if marker_path.is_file():
|
||||
live = marker_path.read_text(encoding="utf-8")
|
||||
if blob != U5_MARKER_BODY or live != U5_MARKER_BODY:
|
||||
base["capability"] = "failed"
|
||||
base["reason"] = "marker bytes differ from the required probe content"
|
||||
base["marker_bytes"] = blob
|
||||
return base
|
||||
base["transport_parent"] = parent
|
||||
base["marker_bytes"] = blob
|
||||
base["capability"] = "PASS"
|
||||
base["reason"] = None
|
||||
return base
|
||||
|
||||
|
||||
class RealAgentCliProbeTests(unittest.TestCase):
|
||||
"""U5 marker probe. Real providers run only behind RUN_REAL_AGENT_EXEC=1."""
|
||||
|
||||
def test_profile_selector_accepts_exactly_one_profile(self) -> None:
|
||||
self.assertEqual(U5_PROFILES, tuple(U5_EXECUTABLE))
|
||||
self.assertFalse(real_probe_opt_in({"RUN_REAL_AGENT_EXEC": "true"}))
|
||||
self.assertFalse(real_probe_opt_in({}))
|
||||
self.assertTrue(real_probe_opt_in({"RUN_REAL_AGENT_EXEC": "1"}))
|
||||
self.assertEqual(
|
||||
require_single_real_profile({"AGENT_EXEC_REAL_PROFILE": "pi"}),
|
||||
"pi",
|
||||
)
|
||||
for raw in ("", " cursor", "cursor ", "cursor,codex", "cursor codex", "nope"):
|
||||
with self.assertRaises(ValueError):
|
||||
require_single_real_profile({"AGENT_EXEC_REAL_PROFILE": raw})
|
||||
|
||||
def test_real_probe_refuses_without_opt_in(self) -> None:
|
||||
self.assertNotEqual(os.environ.get("RUN_REAL_AGENT_EXEC"), "1")
|
||||
with self.assertRaises(RuntimeError) as caught:
|
||||
execute_marker_probe("cursor", offline_bindir=None)
|
||||
self.assertIn("RUN_REAL_AGENT_EXEC=1", str(caught.exception))
|
||||
|
||||
def test_offline_success_checks_marker_transport_and_canonical(self) -> None:
|
||||
outcome = self._run_cursor(
|
||||
_marker_run_body(body=U5_MARKER_BODY, model_receipt=None)
|
||||
)
|
||||
self.assertEqual(outcome["capability"], "PASS", outcome)
|
||||
self.assertEqual(outcome["marker_bytes"], U5_MARKER_BODY)
|
||||
self.assertEqual(outcome["changed_paths"], [U5_MARKER_REL])
|
||||
self.assertEqual(outcome["transport_parent"], outcome["base_head"])
|
||||
self.assertTrue(outcome["transport_parent"])
|
||||
self.assertEqual(outcome["branch"], U5_BRANCH)
|
||||
self.assertIsNone(outcome["configured_model"])
|
||||
self.assertEqual(outcome["model_receipt_status"], "not-observable")
|
||||
self.assertEqual(outcome["run_terminal"], "success")
|
||||
cleanup = outcome["cleanup"]
|
||||
self.assertIsInstance(cleanup, dict)
|
||||
assert isinstance(cleanup, dict)
|
||||
self.assertTrue(cleanup["transport_created"])
|
||||
self.assertTrue(cleanup["workspace_preserved"])
|
||||
self.assertFalse(cleanup["exact_restore"])
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
self.assertFalse(outcome["marker_in_canonical"])
|
||||
self.assertEqual(outcome["host_fallback_authorizations"], 0)
|
||||
|
||||
def test_observable_verified_model_receipt_stays_attached_to_success(self) -> None:
|
||||
outcome = self._run_cursor(
|
||||
_marker_run_body(body=U5_MARKER_BODY, model_receipt="verified")
|
||||
)
|
||||
self.assertEqual(outcome["capability"], "PASS", outcome)
|
||||
self.assertEqual(outcome["model_receipt_status"], "verified")
|
||||
|
||||
def test_model_receipt_mismatch_is_not_pass(self) -> None:
|
||||
outcome = self._run_cursor(
|
||||
_marker_run_body(body=U5_MARKER_BODY, model_receipt="mismatch")
|
||||
)
|
||||
self.assertEqual(outcome["capability"], "failed", outcome)
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertEqual(outcome["model_receipt_status"], "mismatch")
|
||||
|
||||
def test_wrong_marker_is_not_pass(self) -> None:
|
||||
outcome = self._run_cursor(
|
||||
_marker_run_body(body="WRONG\n", model_receipt=None)
|
||||
)
|
||||
self.assertEqual(outcome["capability"], "failed", outcome)
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
|
||||
def test_missing_opencode_is_unavailable_not_pass(self) -> None:
|
||||
outcome = self._run_offline("opencode", help_text="", run_body="exit 0\n", install=False)
|
||||
self.assertEqual(outcome["capability"], "unavailable", outcome)
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertEqual(outcome["attempt_status"], "unavailable")
|
||||
self.assertEqual(outcome["run_terminal"], "host-fallback-authorized")
|
||||
self.assertEqual(outcome["host_fallback_authorizations"], 1)
|
||||
self.assertFalse(outcome["marker_in_canonical"])
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
cleanup = outcome["cleanup"]
|
||||
assert isinstance(cleanup, dict)
|
||||
self.assertTrue(cleanup["skipped"])
|
||||
self.assertFalse(cleanup["transport_created"])
|
||||
|
||||
def test_incompatible_opencode_is_unavailable_not_pass(self) -> None:
|
||||
outcome = self._run_offline(
|
||||
"opencode",
|
||||
help_text="opencode\nusage: opencode\n",
|
||||
run_body=(
|
||||
"mkdir -p src\n"
|
||||
f"printf '%s\\n' 'SHOULD-NOT-RUN' > {U5_MARKER_REL}\n"
|
||||
"exit 0\n"
|
||||
),
|
||||
install=True,
|
||||
)
|
||||
self.assertEqual(outcome["capability"], "unavailable", outcome)
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertEqual(outcome["attempt_status"], "unavailable")
|
||||
self.assertIn("missing required capability", str(outcome["reason"]))
|
||||
self.assertFalse(outcome["marker_in_canonical"])
|
||||
self.assertNotEqual(outcome["run_terminal"], "success")
|
||||
|
||||
def test_launched_failure_without_marker_is_unavailable_not_pass(self) -> None:
|
||||
outcome = self._run_cursor("echo 'auth failed' >&2\nexit 3\n")
|
||||
self.assertEqual(outcome["capability"], "unavailable", outcome)
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertNotEqual(outcome["run_terminal"], "success")
|
||||
self.assertEqual(outcome["host_fallback_authorizations"], 0)
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
self.assertFalse(outcome["marker_in_canonical"])
|
||||
|
||||
@unittest.skipUnless(
|
||||
real_probe_opt_in(),
|
||||
"real CLI probe NOT_RUN: set RUN_REAL_AGENT_EXEC=1 and "
|
||||
"AGENT_EXEC_REAL_PROFILE to exactly one profile",
|
||||
)
|
||||
def test_opt_in_real_profile_probe(self) -> None:
|
||||
profile = require_single_real_profile(dict(os.environ))
|
||||
outcome = execute_marker_probe(
|
||||
profile,
|
||||
offline_bindir=None,
|
||||
timeout_seconds=1800,
|
||||
retain_fixture=True,
|
||||
)
|
||||
print("U5-PROBE " + json.dumps(outcome, sort_keys=True, default=str))
|
||||
if profile == "opencode" and outcome["capability"] == "unavailable":
|
||||
self.assertNotEqual(outcome["capability"], "PASS")
|
||||
self.assertEqual(outcome["attempt_status"], "unavailable")
|
||||
self.assertFalse(outcome["marker_in_canonical"])
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
return
|
||||
self.assertEqual(outcome["capability"], "PASS", outcome)
|
||||
self.assertEqual(outcome["marker_bytes"], U5_MARKER_BODY)
|
||||
self.assertEqual(outcome["changed_paths"], [U5_MARKER_REL])
|
||||
self.assertTrue(outcome["transport_parent"])
|
||||
self.assertEqual(outcome["branch"], U5_BRANCH)
|
||||
self.assertNotEqual(outcome["model_receipt_status"], "mismatch")
|
||||
self.assertTrue(outcome["canonical_preserved"])
|
||||
|
||||
def _run_cursor(self, run_body: str) -> dict[str, object]:
|
||||
return self._run_offline(
|
||||
"cursor",
|
||||
help_text=_cursor_help(),
|
||||
run_body=run_body,
|
||||
install=True,
|
||||
)
|
||||
|
||||
def _run_offline(
|
||||
self,
|
||||
profile: str,
|
||||
*,
|
||||
help_text: str,
|
||||
run_body: str,
|
||||
install: bool,
|
||||
) -> dict[str, object]:
|
||||
fake = tempfile.TemporaryDirectory(prefix="u5-fake-cli-")
|
||||
self.addCleanup(fake.cleanup)
|
||||
bindir = Path(fake.name)
|
||||
if install:
|
||||
_write_probe_double(
|
||||
bindir,
|
||||
U5_EXECUTABLE[profile],
|
||||
help_text=help_text,
|
||||
run_body=run_body,
|
||||
)
|
||||
return execute_marker_probe(
|
||||
profile,
|
||||
offline_bindir=bindir,
|
||||
timeout_seconds=20,
|
||||
retain_fixture=False,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user