Skip to content

test(anolisa): cover adapter observation contracts - #3313

Merged
kongche-jbw merged 1 commit into
mainfrom
test/AGE-8083-adapter-observation-contracts
Sep 18, 2026
Merged

kongche-jbw merged 1 commit into
mainfrom
test/AGE-8083-adapter-observation-contracts

Conversation

@Forrest-ly

Copy link
Copy Markdown
Collaborator

What

Adds behavior-preserving regression coverage for the read-only adapter observation path — the first slice of #3309, as scoped by #3310.

No production evidence model, public API, or behavior changes. The diff is two test files (+705/-2; the 2 deletions are the two use lines that had to grow).

src/anolisa/crates/anolisa-core/tests/adapter_manager.rs (+7 tests)

Drives the real AdapterManager::scan / AdapterManager::status collectors over the existing staged OpenClaw world:

Contract area Test Pins
candidate vs receipt sets scan_candidate_row_carries_no_source_verdict_or_status_row A declared/discovered row with no receipt carries source_status: None and yields zero status rows — receipt-only status stays distinct from candidate scan
source authority scan_receipt_row_reports_available_source A receipt row reports Available with no operator reason
source authority scan_receipt_row_reports_missing_source_after_bundle_loss The receipt outlives its source: row stays visible, flips to Missing with a reason, and the vanished directory stops being offered as resource_root
integrity precedence status_orders_source_authority_before_integrity SourceAvailable leads, ManagedBundleMatches then SourceRevisionMatches follow in fixed order, and the manager's copies are never duplicated by the driver's
integrity precedence status_missing_source_degrades_without_faking_integrity A lost source ⇒ Degraded, but the integrity signals it can no longer read stay Unknown, never False
integrity precedence status_tampered_package_source_degrades_with_false_integrity On-disk tampering is attributed to ManagedBundleMatches=False only; SourceRevisionMatches stays True because tampering is not a package revision change. False outranks UnknownDegraded
read-only boundary scan_and_status_stay_inside_the_read_only_boundary scan spawns no framework query at all; status issues only read-only verbs; neither mutates installed.toml (byte-identical), the package-owned datadir tree, framework-side state, nor takes the install lock. The queries stay auditable via the existing central log + the fake CLI's argv log

src/anolisa/crates/anolisa-cli/src/commands/tier1/status.rs (+3 tests)

Projects the output of a real AdapterManager::scan into AdapterObservation, instead of only the hand-written sample_scan_entry, so drift between the collector and the component status projection fails in test:

  • adapter_observation_projection_preserves_a_real_receipt_scan — field-by-field fidelity, and AdapterSourceStatus → AdapterSourceSnapshot maps 1:1
  • adapter_observation_projection_keeps_a_real_candidate_row_source_free — a real candidate row never gains a source verdict in projection
  • adapter_observation_projection_reports_absent_for_an_unrelated_componentAbsent (source consulted, absence established), not an empty Present

The fixture uses the cosh driver so it needs no framework CLI: detect is side-effect-free, so scan spawns nothing.

Why

#3309 consolidates adapter declaration, resource, receipt and inventory evidence across consumers. These are the shared facts its acceptance criteria name — source authority, candidate versus receipt sets, integrity precedence, read-only boundaries — and before this PR they were either untested or only tested against synthetic ScanEntry values, so a collector/projection mismatch was invisible.

Verification

  • cargo test --workspace --locked — green (adapter_manager 86 → 93; anolisa-cli bin 1106 → 1109; 0 failures)
  • cargo fmt --all --check — clean
  • cargo clippy --all-targets --locked -- -D warnings — clean
  • Mutation-checked for non-vacuity: dropping the projection's source_status mapping fails adapter_observation_projection_preserves_a_real_receipt_scan; reordering the manager's conditions.insert(0, …) / splice(1..1, …) fails status_orders_source_authority_before_integrity. Both mutations reverted.

Environment: unprivileged (non-root) Alibaba Cloud Linux 3, x86_64, rustc 1.93.0, fake framework commands, no network during the test run. #3310 asks for ALinux 4 containers — the suite is host-agnostic (tempdir-rooted layouts, no privilege, no real framework binary) but I could not run it on ALinux 4 itself; flagging that rather than claiming it.

Notes / follow-ups (not changed here)

  • AdapterSourceRevision is built from the recorded package inventory, not from disk. So SourceRevisionMatches cannot see on-disk tampering — only ManagedBundleMatches can. That is coherent (one signal per evidence source) but the doc comment on SourceRevisionMatches says "still equal the enable-time revision" without saying which bytes are compared. Worth a doc-only clarification in a later [anolisa] refactor: build adapter evidence pipeline #3309 slice; not touched here to keep this PR behavior-preserving.
  • Package-ownership provenance (rpm_root_defective / RPM-backend root selection) is covered in adapter_frameworks.rs fixtures, not duplicated here.

Closes #3310
Part of #3309


Tracked in Multica as AGE-8083 (Developer autopilot tick, 2026-09-17 11:00 Asia/Shanghai).

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review baseline: c648fc4fd83f6b4b570f6a724cd3f31022176c12...0ace70d0f5600e46486f9b67f4d051151f3ae20e

[P1] 状态查询的日志断言会被 enable 阶段误满足

src/anolisa/crates/anolisa-core/tests/adapter_manager.rs:4445 读取并检查完整日志。
同一测试在 4373 行先调用 enable;该调用已写入 framework cli
因此,即使 AdapterManager::status 停止记录所有查询,这个测试仍会通过,
无法保护本 PR 声明的状态查询审计契约。

Possible direction: 在 enable 后截取日志偏移,只检查 status 新增的记录,
并把新增记录与 queries 一一对应;补充移除状态查询日志后会失败的回归验证。

@Forrest-ly
Forrest-ly force-pushed the test/AGE-8083-adapter-observation-contracts branch from 0ace70d to a5f5170 Compare September 17, 2026 05:18
@Forrest-ly

Copy link
Copy Markdown
Collaborator Author

@kongche-jbw 已修复,commit a5f5170c(head 0ace70d0a5f5170c)。单 commit PR,按本仓惯例 amend 后 --force-with-lease 锁旧 head 推送;生产代码零改动,diff 仍只在两个测试文件里。

你的判断成立,而且比"断言偏弱"更糟——它是空的。 enable(现 head 4414 行)本身就会经 ManagerOps::record 往 central log 写 framework cli: … 记录,所以原来那句读整个文件的 central_log.contains("framework cli") 永远被安装阶段满足:AdapterManager::status 一条查询都不记,测试照样绿。

修法走你给的方向:按阶段截取 + 与 queries 一一对应。

  1. 在原本重置 argv log 的同一处(4429-4432)多记一个 central log 字节偏移 central_log_offset,与 argv log "只看 read-only 阶段" 的语义对齐——两个证据源现在覆盖同一时间窗。
  2. 末尾不再匹配整个文件,而是把偏移之后新增的行逐行反序列化成 anolisa_core::central_log::LogRecord,筛出 messageframework cli: 开头的记录,剥掉 program token,得到本阶段的 argv 列表,再与 fake CLI 自己记下的 queriesassert_eq!(4492-4496):条数、顺序、内容全部一一对应。上面已断言 scan_queries.is_empty(),所以 queries 就是 status 阶段的全部调用——这一条同时钉住"status 的每条查询都必须留痕"和"status 只发这些查询"。
  3. 新增两个 test-only helper:central_log_len(偏移;文件不存在时返回 0)与 central_log_framework_argv_after(偏移后的 framework cli argv 列表)。后者走 LogRecord 反序列化而不是子串匹配,顺带把"审计痕迹必须是结构化记录、不是一段自由文本"也钉住。

回归验证(你要的"移除状态查询日志后会失败"):把 status 里的两个 ManagerOpsprobe_opsops)都改成 .with_invocation_logging(false),只让 status 静默、enable 照常记录:

  • 新断言 FAILED — panicked at tests/adapter_manager.rs:4492left: []right: ["plugins list"]
  • 同一 mutation 下把断言换回旧的整文件 contains("framework cli"):PASSED —— 精确复现你指出的失效模式

mutation 只用于验证,已还原,未进 commit(git status 干净,head 仍是 a5f5170c)。顺带得到一条事实:这个 staged world 里 status 恰好发一条查询(plugins list),所以"一一对应"也等于钉住了查询条数,将来 #3309 合并 evidence pipeline 时若多打或少打一次 framework 查询都会在这里失败。

本地验证(toolchain 用 1.93.0,仓库 pin 的 1.93.1 在镜像上不存在):cargo test -p anolisa-core 全绿(1102 + 66 + 93 + 1)、cargo fmt --all -- --check 无 diff、cargo clippy --workspace --all-targets -- -D warnings 退出 0。

请重新 review。

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review baseline: a9a371978d3c214e1d8b443c0a4fdd5547b2c10b...a5f5170cc47c57e938509b53d9b27f7d4c2d9709

[P2] 按空格截取 program 会让合法临时路径触发失败

src/anolisa/crates/anolisa-core/tests/adapter_manager.rs:4156
split_once(' ') 删除可执行文件 token,但 tempfile 会继承 TMPDIR
TMPDIR 路径含空格时,fake binary 的绝对路径也含空格,helper 只截掉
第一个路径片段;正常的 plugins list 因而被解析为
space/.../openclaw plugins list,与 fake CLI 记录的 plugins list 不同。
我用含空格的 TMPDIR 运行该测试可稳定复现失败,生产调用与审计日志均正常。

Possible direction: 将精确的 world.fake_bin 路径传给 helper 并按完整前缀剥离,
或让两侧记录相同的完整 argv;补一条临时目录路径含空格的回归验证。

Add behavior-preserving regression coverage for the read-only adapter
observation path, as the first slice of #3309 (tracked by #3310). No
production evidence model, public API, or behavior changes: the diff is
two test files.

anolisa-core tests/adapter_manager.rs drives the real scan and status
collectors over the existing staged OpenClaw world:

- candidate versus receipt sets: a declared/discovered row with no
  receipt carries no source verdict at all and yields no status row, so
  receipt-only status stays distinct from candidate scan
- source authority: a receipt row reports Available, and keeps reporting
  (with an operator reason) after the package-owned bundle is lost, while
  the vanished directory stops being offered as a resource root
- integrity precedence: SourceAvailable leads the projection, the two
  package-owned integrity signals follow in a fixed order, and the
  manager's copies are never duplicated by the driver's
- a lost source degrades but leaves integrity Unknown ("could not
  verify"), never False ("verified absent")
- a tampered package-owned file is attributed to ManagedBundleMatches
  only; SourceRevisionMatches stays True, because on-disk tampering is
  not a package revision change
- read-only boundary: scan spawns no framework query at all, status
  issues only read-only verbs, and neither mutates installed.toml, the
  package-owned tree, framework-side state, nor takes the install lock.
  The audit trail is asserted per phase: the central-log records appended
  after enable are parsed as LogRecords and must equal, one record per
  query and in order, the argv the fake CLI logged during scan+status, so
  the install phase's own records cannot satisfy the assertion. The helper
  strips the exact staged program path instead of everything up to the
  first space, so the comparison also holds when the temp prefix -- and
  therefore the fake CLI's absolute path -- contains one
- a staged prefix whose path contains a space: tempfile inherits TMPDIR,
  so a developer machine or CI runner with a space in its temp root stages
  a spaced CLI path. The new spaced-prefix audit test pins that shape into
  the world and requires the same central-log to argv equality there

anolisa-cli status.rs projects the output of a real AdapterManager::scan
into AdapterObservation, instead of only the hand-written sample entry,
so drift between the collector and the component status projection fails
in test. Covers receipt-backed fidelity field by field, a candidate row
staying source-free, and an unrelated component projecting Absent rather
than an empty Present.

All four mutation-checked: dropping the projection's source_status
mapping, reordering the manager's condition insertion, silencing status's
framework invocation logging, and restoring the first-space strip in the
audit helper each fail the intended test. The third passed before the
per-phase audit assertion, because matching the whole log file was
already satisfied by enable; the fourth fails the spaced-prefix test and
also the read-only boundary test when TMPDIR contains a space. cargo test
--workspace, cargo fmt --all --check and cargo clippy --all-targets
--locked -- -D warnings are green.

Co-authored-by: multica-agent <github@multica.ai>
@Forrest-ly
Forrest-ly force-pushed the test/AGE-8083-adapter-observation-contracts branch from a5f5170 to 18b7e6f Compare September 17, 2026 07:43
@Forrest-ly

Copy link
Copy Markdown
Collaborator Author

P2 已修复,commit 18b7e6fc(head a5f5170c18b7e6fc,仍是单 commit,amend 后 --force-with-lease 锁旧 head 推送)。生产代码零改动,相对上一 head 的增量只有 adapter_manager.rs 一个测试文件,+76/-10。

一、先确认你的复现

成立,而且我按你的方式跑出了同样的结果。把 TMPDIR 指到含空格的目录、helper 保持旧实现:

thread 'scan_and_status_stay_inside_the_read_only_boundary' panicked at adapter_manager.rs:4546:
assertion `left == right` failed: the central log must record exactly the framework queries status issued
  left: ["tmp/.tmpEPr7cT/openclaw plugins list"]
 right: ["plugins list"]

生产侧和审计日志本身都是对的:record() 把命令拼成 [program, args…].join(" ")manager.rs:2671-2683),fake CLI 记的是 "$*"(不含 program)。错的只是 helper 用第一个空格当 program 边界,于是把路径尾巴当成了 argv 的一部分——正如你说的,一个纯测试侧的解析缺陷会让正常主机上的正常调用被判成不一致。

二、修法:走你给的第一条路,按完整前缀剥离

helper 签名加上被剥离的 program,不再猜边界:

fn central_log_framework_argv_after(path: &Path, offset: u64, program: &Path) -> Vec<String>

调用方把 &world.fake_bin 传进去,helper 里 strip_prefix(program) 再吃掉一个分隔空格。因为生产侧就是"program + 一个空格 + args"拼出来的,按精确 program 剥离是逐字节成立的,路径里有几个空格都无所谓。

另外把静默失败改成显式失败:审计命令若不以被 stage 的 CLI 路径开头,helper 直接 panic 并打印两个字符串。以前 unwrap_or_default() 会在 program 对不上时返回空串,那种情况下断言会以"少了一条记录"的形式失败,看不出真正原因是记录格式变了;现在会当场说明。

三、回归测试

新增 central_log_argv_audit_survives_a_staged_prefix_with_spaces。为了不依赖"宿主 TMPDIR 恰好有空格",我把 stage() 拆成 stage() + stage_in(root),测试自己把 world 根到 <tmp>/prefix with spaces/<tmpXXXX> 下,先断言前置条件 world.fake_bin 里确实有空格,再要求同一套"enable 之后追加的 central-log 记录 == fake CLI 记到的 argv"逐条相等。

不是空转——只把 helper 的剥离逻辑改回 split_once(' '),新测试立刻红:

  left: ["with spaces/.tmp6RXKn1/openclaw plugins list"]
 right: ["plugins list"]

四、验证

  • cargo test -p anolisa-core --test adapter_manager94 passed(原 93 + 新增 1)
  • 同样这套 suite 在含空格的 TMPDIR 下再跑一遍:94 passed
  • cargo test --workspace2585 passed, 1 ignored(21 suites)
  • cargo fmt --all -- --check:干净
  • cargo clippy --all-targets --locked -- -D warnings:干净

工具链照旧说明一句:rust-toolchain.toml 钉的 1.93.1 在镜像上仍是 404,实际用 1.93.0 跑(RUSTUP_TOOLCHAIN=1.93.0),未改 toolchain 文件。.github/ 下 0 个文件改动。

@kongche-jbw
kongche-jbw self-requested a review September 18, 2026 06:35

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review baseline: 1d2534ec0603ae51d8f600eeb23af679dc8e4392...18b7e6fc701e7b65b8be96ad83af7e76edf5e5e1

未发现阻塞问题。

Remaining risks:

  • 未在 ALinux 4 容器复跑完整 workspace。
  • PR 正文测试计数仍为 93,当前目标测试实际为 94;最新评论已说明新增回归。

Verification scope:

  • 完整 diff、相邻生产实现、既有 reviews/comments/threads、当前 CI。
  • adapter_manager 94/94;CLI 投影 3/3;fmt、clippy、git diff --check 通过。

@kongche-jbw kongche-jbw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kongche-jbw
kongche-jbw merged commit 5d684af into main Sep 18, 2026
27 checks passed
@kongche-jbw
kongche-jbw deleted the test/AGE-8083-adapter-observation-contracts branch September 18, 2026 06:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[anolisa] test: cover adapter observation contracts

2 participants