test(issue-5): harden watcher evidence validation and add codex pass review report
- Enforce evidence-root confinement for capture_ref and 1:1 capture citation uniqueness - Add pane identity and backend identity cross-validation - Add bidirectional agreement checks between watcher/herdr manifests and scenario fixtures - Expand contract test suite to 41 tests (100% pass) - Document evidence confinement rules in herdr-runtime.md and update task.md - Archive Codex 4th review report (Job 1bd561e6: [VERDICT: PASS])
This commit is contained in:
1 parent
19d798bf8f
commit
fc0bd96d8a
6 files changed
+286
-15
No files matched your search
@@ -0,0 +1,29 @@
|
|||||||
|
# Issue #5 Phase 0 final revision review
|
||||||
|
|
||||||
|
Job: 1bd561e6
|
||||||
|
Reviewer: planner-reviewer-codex-01 (planner,reviewer)
|
||||||
|
Scope: commit 19d798b and the current five-file documentation/contract-test diff.
|
||||||
|
|
||||||
|
No blocking findings remain in the requested revision. No supplementary implementation edits were necessary.
|
||||||
|
|
||||||
|
## Findings resolved
|
||||||
|
|
||||||
|
- **Previous Codex P1:** Observed evidence now dispatches through explicit supported source kinds. Event/filesystem/harness observations fail closed until independent evidence schemas exist; their simulated fixtures remain accepted. Herdr evidence must pass the native validator, contain mandatory agent_status, match every declared native field, and correspond to the scenario session identity. Current additions also check pane/backend identity, evidence-root confinement, repeated capture citations, and manifest/file agreement.
|
||||||
|
- Independently reran the exact prior finalized_candidate mutation that cited a Claude idle capture for every observed item. It now raises AssertionError at the first event item because no observed event schema exists. This closes the reproduced false-evidence path.
|
||||||
|
- **Claude Finding 2:** baseline.md now cites registry.py:38 for VALID_STATUSES. Verified that definition directly.
|
||||||
|
- Existing native/composite separation and simulated positive scenarios are preserved. The changes remain Phase 0 fixture validation and documentation, not a claim that the future production watcher or complete execution-attribution mechanism is implemented.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `./.venv/bin/python -m pytest tests/test_issue5_phase0_contracts.py -q`: **41 passed in 0.02s**.
|
||||||
|
- `./.venv/bin/python -m pytest --collect-only -q`: **510 tests collected**.
|
||||||
|
- Independent previous-exploit reproduction: **rejected**.
|
||||||
|
- `git diff --check`: clean.
|
||||||
|
- `git diff -- .agents/skills`: empty; current changes are four contract documents and one test file.
|
||||||
|
- Full-suite evidence in task.md reports **499 passed in 848.90s** at 19d798b, followed by the current 41 contract tests after the test/document-only additions. This reviewer independently ran the current focused tests and collection, not another full suite.
|
||||||
|
|
||||||
|
Nonblocking documentation note: interpret the roadmap's 510-test row as its acceptance target; task.md correctly distinguishes the 499-test full run from current 510-test collection and 41-test focused execution. It should not be quoted as an independently executed 510-test full run.
|
||||||
|
|
||||||
|
This PASS applies to the reviewed Phase 0 corrections. Future runtime/CAS/rollback implementation remains subject to the staged gates already documented.
|
||||||
|
|
||||||
|
[VERDICT: PASS]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
- **Issue**: [Gitea Issue #5 — `[Refactor] lib.sh 작업 위임 로직 및 run_loop.sh 오케스트레이터의 Python 모듈화 (lib_py 승격)`](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
- **Issue**: [Gitea Issue #5 — `[Refactor] lib.sh 작업 위임 로직 및 run_loop.sh 오케스트레이터의 Python 모듈화 (lib_py 승격)`](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
||||||
- **Baseline Commit**: `47640e2fd7b108be29446a9384a343727e87edf8`
|
- **Baseline Commit**: `47640e2fd7b108be29446a9384a343727e87edf8`
|
||||||
- **Environment Baseline**: Python 3.9.6, Herdr 0.8.2, macOS Darwin 24.3.0
|
- **Environment Baseline**: Python 3.9.6, Herdr 0.8.2, macOS Darwin 24.3.0
|
||||||
- **Test Suite Baseline**: 469 tests collected initially; 499 tests collected with Phase 0 contract suite (+30 contract & mutation tests)
|
- **Test Suite Baseline**: 469 tests collected initially; 510 tests collected with Phase 0 contract suite (+41 contract & mutation tests)
|
||||||
- **Status**: Phase 0 Baseline Contract (Observed vs. Proposed vs. Gap)
|
- **Status**: Phase 0 Baseline Contract (Observed vs. Proposed vs. Gap)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 📑 Herdr Runtime Contract and Backend Capability Matrix
|
# 📑 Herdr Runtime Contract and Backend Capability Matrix
|
||||||
|
|
||||||
- **Document Version**: Rev.3 (Post Cross-Review Corrections)
|
- **Document Version**: Rev.5 (Evidence Source-Kind Dispatch & Identity Correspondence — §3-1 신설)
|
||||||
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
||||||
- **Target Herdr Version**: `herdr 0.8.2`
|
- **Target Herdr Version**: `herdr 0.8.2`
|
||||||
- **Scope**: Native Herdr API Contract, External Collector Specification, and Backend Matrix
|
- **Scope**: Native Herdr API Contract, External Collector Specification, and Backend Matrix
|
||||||
@@ -76,6 +76,23 @@ Herdr 네이티브 페이로드와 Multi-Agent-Mux 오케스트레이션 메타
|
|||||||
|
|
||||||
- **불변식**: Herdr 원본 응답 페이로드에는 `job_id`, `attempt_id`, `report phase`, `expected_decision` 등의 MAM 내부 필드를 절대 주입하지 않습니다.
|
- **불변식**: Herdr 원본 응답 페이로드에는 `job_id`, `attempt_id`, `report phase`, `expected_decision` 등의 MAM 내부 필드를 절대 주입하지 않습니다.
|
||||||
|
|
||||||
|
### 3-1. Watcher 타임라인 증거 스키마 및 `source_kind` 분기 계약 (Rev.5)
|
||||||
|
|
||||||
|
Watcher 복합 시나리오(`tests/fixtures/issue_5/watcher/*.json`)의 각 타임라인 항목은 아래 계약을 따르며, `tests/test_issue5_phase0_contracts.py`의 `validate_watcher_scenario`가 이를 기계적으로 강제합니다. 위반 시 fixture 검증은 **fail-closed(거부)** 입니다.
|
||||||
|
|
||||||
|
| 규칙 | 내용 | 강제 지점 |
|
||||||
|
|---|---|---|
|
||||||
|
| **지원 source_kind 집합** | `herdr`, `event`, `filesystem`, `harness` 4종만 허용. 그 외 값은 provenance와 무관하게 거부 | `SUPPORTED_SOURCE_KINDS` |
|
||||||
|
| **관측(observed) 증거 스키마** | 독립 증거 스키마가 구현된 kind만 `source_provenance: observed`를 선언할 수 있음. **현재 `herdr`만 해당.** `event`/`filesystem`/`harness`는 스키마가 생길 때까지 `simulated`(또는 `synthetic`)로만 기록하며, `observed`로 표기 시 `capture_ref`가 유효하더라도 즉시 거부 | `OBSERVED_EVIDENCE_SCHEMAS` |
|
||||||
|
| **capture_ref 격리** | repo 상대 경로여야 하며(절대 경로 금지), 해석 결과가 `tests/fixtures/issue_5/herdr/` 하위여야 함(`..` 우회 금지). `.json`이고 `schema_version 1.0.0`·`provenance observed`·`capture_id`를 갖는 네이티브 캡처만 증거로 인정 | `_load_observed_capture_ref` |
|
||||||
|
| **Herdr 항목 대응성** | ① 참조 캡처는 Layer 1 네이티브 계약(`validate_native_capture`)을 재통과 ② 항목 payload는 비어있지 않은 dict이며 `agent_status`를 **필수 선언**(누락 시 비교 생략이 아닌 거부) ③ 캡처는 `result.agent` 객체를 포함해야 함(`wait` timeout 등 nonzero-exit 캡처는 agent_status를 입증할 수 없으므로 거부) ④ 선언된 **모든** 필드가 agent 객체의 네이티브 필드에 존재하고 값이 정확히 일치(비네이티브 필드 선언 시 거부) | `_validate_observed_herdr_item` |
|
||||||
|
| **정체성 대응 (4중 귀속 연동)** | 캡처의 `agent_session.value` == `dispatch_context.agent_session_uuid`, 캡처의 `pane_id`가 `dispatch_context.pane_process_generation`(`<pane_id>:<pid>:<start>`)의 선두 구성요소여야 함. 시나리오 `backend`가 `backend_agnostic`이 아니면 캡처 `backend`와 일치해야 함 | `_validate_observed_herdr_item` |
|
||||||
|
| **1캡처 = 1관측** | 하나의 `capture_id`는 시나리오 내 관측 항목 하나만 입증할 수 있음(동일 스냅샷의 재인용 금지) | `validate_watcher_scenario` |
|
||||||
|
| **provenance 정직성** | observed·simulated 혼재 시 전체 `provenance`는 `mixed`, 전량 관측 시 `observed_composite`, 전량 모의 시 `simulated`여야 함 (`synthetic` 항목은 비관측으로 집계) | `validate_watcher_scenario` |
|
||||||
|
| **매니페스트 정합성** | `watcher/manifest.json`의 `provenance`/`expected_decision`은 시나리오 파일과 일치해야 하며, `herdr/manifest.json`의 observed 슬롯은 동일 backend의 캡처가 슬롯명과 같은 `agent_status`(또는 `wait_timeout` 슬롯은 `error.code=timeout`)를 기록해야 함. 디스크의 모든 캡처는 정확히 하나의 observed 슬롯에 색인되어야 함(고아 캡처 금지) | `validate_watcher_manifest`, `validate_herdr_manifest` |
|
||||||
|
|
||||||
|
> 📌 이 계약은 **fixture 검증 계약**이며 프로덕션 Watcher 구현(Phase 2a)이 아닙니다. Phase 2a의 `watcher.py`는 이 표의 "관측 증거 스키마" 열을 확장(event/filesystem 스키마 추가)할 때 `OBSERVED_EVIDENCE_SCHEMAS`를 함께 갱신해야 하며, 스키마 없는 kind를 관측으로 승격하는 일은 허용되지 않습니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. 6개 백엔드 기능 및 실측 증거 행렬 (Capability Matrix)
|
## 4. 6개 백엔드 기능 및 실측 증거 행렬 (Capability Matrix)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
| 단계 | 작업 목표 | 세부 구현 내용 | 필수 완료 게이트 (Gate Criteria) | 롤백 절차 (Rollback Path) |
|
| 단계 | 작업 목표 | 세부 구현 내용 | 필수 완료 게이트 (Gate Criteria) | 롤백 절차 (Rollback Path) |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| **Phase 0**<br>*(현재)* | **사전 계약 고정 및 Fixture 수집** | • CLI, 이벤트, 리포트, 락 계약 기준선 문서화<br>• Herdr 0.8.2 네이티브 캡처 및 복합 Watcher 시나리오 2계층 분리<br>• 6개 백엔드 실측 증거 행렬 수집 | • 499개 전체 테스트 수집 및 회귀 0-Fail (100% 통과)<br>• Fixture 스키마 및 증거 경계 검증 통과<br>• 프로덕션 코드 무변경 diff 검증 | 미추적 산출물 즉시 삭제 (`rm -rf docs/contracts/issue-5 tests/fixtures/issue_5 tests/test_issue5_phase0_contracts.py`) |
|
| **Phase 0**<br>*(현재)* | **사전 계약 고정 및 Fixture 수집** | • CLI, 이벤트, 리포트, 락 계약 기준선 문서화<br>• Herdr 0.8.2 네이티브 캡처 및 복합 Watcher 시나리오 2계층 분리<br>• 6개 백엔드 실측 증거 행렬 수집 | • 510개 전체 테스트 수집 및 회귀 0-Fail (100% 통과)<br>• Fixture 스키마 및 증거 경계 검증 통과<br>• 프로덕션 코드 무변경 diff 검증 | 미추적 산출물 즉시 삭제 (`rm -rf docs/contracts/issue-5 tests/fixtures/issue_5 tests/test_issue5_phase0_contracts.py`) |
|
||||||
| **Phase 1** | **`lib_py/delegation/` 추출** | • `lib.sh` L1833 이하 위임 헬퍼를 파이썬 서비스 클래스로 추출<br>• `start_watchdog`을 내부 명칭 `ensure_subscriber_daemon`으로 명명(기존 함수 별칭 보존)<br>• 장수 프로세스 단일 MQTT 연결 재사용 프로토타입 | • CLI/non-fatal/HMAC/seq/디스크 폴백 회귀 100% 통과<br>• 데몬 수명 및 PID 계약 일치 확인<br>• **자동완료 정책 변경 없음** | `lib.sh` 위임 헬퍼 호출을 이전 인라인/셸 방식으로 즉시 복원 |
|
| **Phase 1** | **`lib_py/delegation/` 추출** | • `lib.sh` L1833 이하 위임 헬퍼를 파이썬 서비스 클래스로 추출<br>• `start_watchdog`을 내부 명칭 `ensure_subscriber_daemon`으로 명명(기존 함수 별칭 보존)<br>• 장수 프로세스 단일 MQTT 연결 재사용 프로토타입 | • CLI/non-fatal/HMAC/seq/디스크 폴백 회귀 100% 통과<br>• 데몬 수명 및 PID 계약 일치 확인<br>• **자동완료 정책 변경 없음** | `lib.sh` 위임 헬퍼 호출을 이전 인라인/셸 방식으로 즉시 복원 |
|
||||||
| **Phase 2a** | **`lib_py/loop/` 코어 & Shadow Watcher** | • `engine.py` (단일 전이 주체), `watcher.py` (`herdr agent wait` 래핑) 구현<br>• 모든 Writer에 대한 단일 CAS 트랜잭션 경계 구현<br>• 자동완료를 수행하지 않고 "Would-Complete" 로깅만 수행하는 Shadow 모드 가동 | • 락 내 비동기/I/O 배제 검증<br>• 소켓 단절/스냅샷 재연결 백오프 검증<br>• Shadow 모드가 레지스트리 상태를 쓰지 않음을 검증 | 1) Shadow 모니터 프로세스 종료 및 인메모리 큐 폐기<br>2) CAS 서비스는 하위 호환 모드로 유지하거나 `MAM_ENGINE_CAS=0` 플래그로 비활성화하여 레거시 무조건 필드 병합(`update_job_status`)으로 복귀<br>3) 기존 셸 루프 상태 및 레코드 불변 유지 |
|
| **Phase 2a** | **`lib_py/loop/` 코어 & Shadow Watcher** | • `engine.py` (단일 전이 주체), `watcher.py` (`herdr agent wait` 래핑) 구현<br>• 모든 Writer에 대한 단일 CAS 트랜잭션 경계 구현<br>• 자동완료를 수행하지 않고 "Would-Complete" 로깅만 수행하는 Shadow 모드 가동 | • 락 내 비동기/I/O 배제 검증<br>• 소켓 단절/스냅샷 재연결 백오프 검증<br>• Shadow 모드가 레지스트리 상태를 쓰지 않음을 검증 | 1) Shadow 모니터 프로세스 종료 및 인메모리 큐 폐기<br>2) CAS 서비스는 하위 호환 모드로 유지하거나 `MAM_ENGINE_CAS=0` 플래그로 비활성화하여 레거시 무조건 필드 병합(`update_job_status`)으로 복귀<br>3) 기존 셸 루프 상태 및 레코드 불변 유지 |
|
||||||
| **Phase 2b** | **이중 감지 자동완료 활성화 (Flag-Gated)** | • 4중 실행 귀속 + 리포트 확정 계약 + 단일 CAS 전이 게이트 연동<br>• Pending Notification 지연 알림 및 크래시 복구 구현<br>• 검증된 백엔드/작업에 한해 opt-in 플래그로 활성화 (기본값 OFF) | • 부수효과 작업 예외 및 무판정 fail-closed 테스트 완료<br>• 경쟁/재시작 시나리오 통과<br>• 플래그 OFF 시 기존 explicit event 경로 100% 보존 | 환경변수/플래그 `MAM_AUTO_COMPLETE=0` 설정; 이미 확정된 terminal 레코드는 보존하고 신규 자동완료만 중단 |
|
| **Phase 2b** | **이중 감지 자동완료 활성화 (Flag-Gated)** | • 4중 실행 귀속 + 리포트 확정 계약 + 단일 CAS 전이 게이트 연동<br>• Pending Notification 지연 알림 및 크래시 복구 구현<br>• 검증된 백엔드/작업에 한해 opt-in 플래그로 활성화 (기본값 OFF) | • 부수효과 작업 예외 및 무판정 fail-closed 테스트 완료<br>• 경쟁/재시작 시나리오 통과<br>• 플래그 OFF 시 기존 explicit event 경로 100% 보존 | 환경변수/플래그 `MAM_AUTO_COMPLETE=0` 설정; 이미 확정된 terminal 레코드는 보존하고 신규 자동완료만 중단 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 📑 Issue #5 Task Checklist & Verification Evidence
|
# 📑 Issue #5 Task Checklist & Verification Evidence
|
||||||
|
|
||||||
- **Document Version**: Rev.3 (Post Cross-Review Corrections)
|
- **Document Version**: Rev.5 (Evidence Dispatch Hardening — P0-16/P0-18/P0-20 증거 갱신)
|
||||||
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
- **Issue**: [Gitea Issue #5](https://git.godopu.com/tmpl/multi-agent-mux/issues/5)
|
||||||
- **Status**: Phase 0 Complete / Phase 1–4 Staged
|
- **Status**: Phase 0 Complete / Phase 1–4 Staged
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
## Phase 0: 사전 계약 기준선 및 Fixture 수집 (이번 실행 범위)
|
## Phase 0: 사전 계약 기준선 및 Fixture 수집 (이번 실행 범위)
|
||||||
|
|
||||||
- [x] **P0-01** 기준 commit, dirty diff, Herdr/Python 버전, pytest collection 수 기록.
|
- [x] **P0-01** 기준 commit, dirty diff, Herdr/Python 버전, pytest collection 수 기록.
|
||||||
- *증거*: Commit `47640e2`, Herdr `0.8.2`, Python `3.9.6`, Pytest `469 tests collected initially` ➔ `499 tests collected with Phase 0 suite` (+30 contract & mutation tests).
|
- *증거*: Commit `47640e2`, Herdr `0.8.2`, Python `3.9.6`, Pytest `469 tests collected initially` ➔ `510 tests collected with Phase 0 suite` (+41 contract & mutation tests).
|
||||||
- [x] **P0-02** `docs/contracts/issue-5/baseline.md` 작성: 공개 CLI/default/invalid input/stdout-stderr/exit/interpreter/path/signal 계약과 소스·기존 테스트 매핑.
|
- [x] **P0-02** `docs/contracts/issue-5/baseline.md` 작성: 공개 CLI/default/invalid input/stdout-stderr/exit/interpreter/path/signal 계약과 소스·기존 테스트 매핑.
|
||||||
- *증거*: `docs/contracts/issue-5/baseline.md` §2 (`--all-reviewer` 우선순위 및 L192–196 소스 매핑 반영).
|
- *증거*: `docs/contracts/issue-5/baseline.md` §2 (`--all-reviewer` 우선순위 및 L192–196 소스 매핑 반영).
|
||||||
- [x] **P0-03** event schema/HMAC/seq/retain/audit/디스크 fallback 및 publisher 실패 후 로컬 commit 동작 기록.
|
- [x] **P0-03** event schema/HMAC/seq/retain/audit/디스크 fallback 및 publisher 실패 후 로컬 commit 동작 기록.
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
- *증거*: `tests/fixtures/issue_5/herdr/manifest.json`, `tests/fixtures/issue_5/watcher/manifest.json`.
|
- *증거*: `tests/fixtures/issue_5/herdr/manifest.json`, `tests/fixtures/issue_5/watcher/manifest.json`.
|
||||||
- [x] **P0-16** 두 계층 schema/reference/coverage 테스트 추가: native payload에 job/attempt/report phase 주입 거부, 오류와 상태 분리, simulated를 observed로 표기 거부. 운영 session 쓰기 금지.
|
- [x] **P0-16** 두 계층 schema/reference/coverage 테스트 추가: native payload에 job/attempt/report phase 주입 거부, 오류와 상태 분리, simulated를 observed로 표기 거부. 운영 session 쓰기 금지.
|
||||||
- *증거*: `tests/test_issue5_phase0_contracts.py`.
|
- *증거*: `tests/test_issue5_phase0_contracts.py`.
|
||||||
|
- *Rev.5 보강 (Codex [P1] 대응)*: `source_kind` 명시적 분기(`SUPPORTED_SOURCE_KINDS`), 관측 증거 스키마 미구현 kind(event/filesystem/harness)의 `observed` 표기 fail-closed 거부, Herdr 항목의 필수 필드·전 필드 대응·세션 UUID/pane/backend 정체성 대응, `capture_ref` 증거 루트 격리, 1캡처=1관측, 매니페스트↔파일 정합성 검증. 계약 명세는 `herdr-runtime.md` §3-1.
|
||||||
- [x] **P0-16a** watcher 복합 시나리오 7종 작성: initial idle, tool active, report pause, finalized candidate, blocked, disconnect, generation restart. source별 증거/가상 시계/expected policy를 구분.
|
- [x] **P0-16a** watcher 복합 시나리오 7종 작성: initial idle, tool active, report pause, finalized candidate, blocked, disconnect, generation restart. source별 증거/가상 시계/expected policy를 구분.
|
||||||
- *증거*: `tests/fixtures/issue_5/watcher/*.json` (7개 시나리오 완료).
|
- *증거*: `tests/fixtures/issue_5/watcher/*.json` (7개 시나리오 완료).
|
||||||
- [x] **P0-16b** `herdr-runtime.md`에 native API·단일 wait·외부 collector·file/event/dispatch source 경계를 명시.
|
- [x] **P0-16b** `herdr-runtime.md`에 native API·단일 wait·외부 collector·file/event/dispatch source 경계를 명시.
|
||||||
@@ -47,11 +48,12 @@
|
|||||||
- [x] **P0-17** Phase 1–4 `implementation_plan.md`/`task.md` 작성, live-helper version/공통 CAS/notification 복구 결정 기록.
|
- [x] **P0-17** Phase 1–4 `implementation_plan.md`/`task.md` 작성, live-helper version/공통 CAS/notification 복구 결정 기록.
|
||||||
- *증거*: `docs/contracts/issue-5/implementation_plan.md`, `docs/contracts/issue-5/task.md`.
|
- *증거*: `docs/contracts/issue-5/implementation_plan.md`, `docs/contracts/issue-5/task.md`.
|
||||||
- [x] **P0-18** 관련 CLI/lock/rebuttal/adapter 테스트와 전체 suite 실행; 수집·실행·통과·실패 숫자 분리.
|
- [x] **P0-18** 관련 CLI/lock/rebuttal/adapter 테스트와 전체 suite 실행; 수집·실행·통과·실패 숫자 분리.
|
||||||
- *증거*: 전체 484개 수집, 선행 전체 실행 481개 통과(0 failed, 0 skipped, 786.18s), 15개 계약/변이 테스트 전체 통과, 집중 코어 회귀 155개 통과(60.48s).
|
- *증거 (Rev.3 당시)*: 전체 484개 수집, 선행 전체 실행 481개 통과(0 failed, 0 skipped, 786.18s), 15개 계약/변이 테스트 전체 통과, 집중 코어 회귀 155개 통과(60.48s).
|
||||||
|
- *증거 (Rev.5, job `fb7fadd4`)*: 전체 suite 실행 **499 passed, 0 failed, 0 skipped (848.90s)** — 커밋 `19d798b` 기준선(계약 테스트 30개 포함)에서 실행. Rev.5 보강 후 계약 테스트 파일 단독 재실행 **41 passed** (`+11`), 수집 총계 **510** (`--collect-only`). Rev.5 변경은 `tests/test_issue5_phase0_contracts.py` 및 `docs/`에 국한되어 나머지 469개 테스트에 영향 없음.
|
||||||
- [x] **P0-19** 문서 링크/JSON/git diff --check/production 코드 무변경 확인.
|
- [x] **P0-19** 문서 링크/JSON/git diff --check/production 코드 무변경 확인.
|
||||||
- *증거*: `git diff --check` 클린, 공백 린트 완료, 프로덕션 `.sh`/`.py` 무변경 (오직 `docs/`, `tests/` 변경).
|
- *증거*: `git diff --check` 클린, 공백 린트 완료, 프로덕션 `.sh`/`.py` 무변경 (오직 `docs/`, `tests/` 변경).
|
||||||
- [x] **P0-20** 결과 보고: Phase 0 산출물 완료 여부, backend별 native live gap·mixed/simulated scenario 현황, 남은 gate를 별도 명시하고 peer review 요청.
|
- [x] **P0-20** 결과 보고: Phase 0 산출물 완료 여부, backend별 native live gap·mixed/simulated scenario 현황, 남은 gate를 별도 명시하고 peer review 요청.
|
||||||
- *증거*: `.mam/jobs/61f2fa87/agy-reports/report-final.md`.
|
- *증거*: `.mam/jobs/61f2fa87/agy-reports/report-final.md` (Rev.3 최초 보고), `.mam/jobs/f29cb24e/claude-code-reports/report-final.md` (Codex [P1]·Claude [Finding 2] 정정, 커밋 `19d798b`), `.mam/jobs/fb7fadd4/claude-reports/report-final.md` (Rev.5 자체 재리뷰 및 보강).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ Validates:
|
|||||||
accepted as 'observed' when an evidence schema exists for that source kind (currently Herdr only).
|
accepted as 'observed' when an evidence schema exists for that source kind (currently Herdr only).
|
||||||
Event / filesystem / harness items have no independent evidence schema yet and are fail-closed when
|
Event / filesystem / harness items have no independent evidence schema yet and are fail-closed when
|
||||||
labeled 'observed'; they must remain 'simulated' until such schemas exist.
|
labeled 'observed'; they must remain 'simulated' until such schemas exist.
|
||||||
|
8. Evidence identity (Rev.5 self-review hardening): observed capture_refs must be repo-relative paths confined
|
||||||
|
to the native Herdr evidence root; one capture may substantiate at most one timeline observation; the
|
||||||
|
capture's session UUID and pane id must match the scenario's 4-fold attribution; a backend-specific
|
||||||
|
scenario may only cite captures of that backend; and both fixture manifests must agree with the files
|
||||||
|
they index (no drift, no orphan captures).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
@@ -48,6 +53,12 @@ OBSERVED_EVIDENCE_SCHEMAS = {"herdr"}
|
|||||||
# against the native capture cannot be skipped.
|
# against the native capture cannot be skipped.
|
||||||
HERDR_MANDATORY_ITEM_FIELDS = ("agent_status",)
|
HERDR_MANDATORY_ITEM_FIELDS = ("agent_status",)
|
||||||
|
|
||||||
|
# Native agent_status values a Herdr manifest slot may be named after (slot name == recorded agent_status).
|
||||||
|
HERDR_AGENT_STATUS_SLOTS = {"idle", "working", "blocked", "done", "unknown"}
|
||||||
|
|
||||||
|
# Scenario backend marker meaning "any backend's capture may be cited".
|
||||||
|
BACKEND_AGNOSTIC = "backend_agnostic"
|
||||||
|
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# Validation Engine Functions (Used for both real fixtures & negative tests)
|
# Validation Engine Functions (Used for both real fixtures & negative tests)
|
||||||
@@ -103,10 +114,24 @@ def _load_observed_capture_ref(index: int, item: dict) -> "tuple[Path, dict]":
|
|||||||
assert "capture_ref" in item and item["capture_ref"], (
|
assert "capture_ref" in item and item["capture_ref"], (
|
||||||
f"Timeline item [{index}] is marked observed but lacks non-empty 'capture_ref'"
|
f"Timeline item [{index}] is marked observed but lacks non-empty 'capture_ref'"
|
||||||
)
|
)
|
||||||
ref_path = REPO_ROOT / item["capture_ref"]
|
raw_ref = str(item["capture_ref"])
|
||||||
|
assert not Path(raw_ref).is_absolute(), (
|
||||||
|
f"Observed timeline item [{index}] capture_ref must be a repo-relative path, got absolute '{raw_ref}'"
|
||||||
|
)
|
||||||
|
ref_path = REPO_ROOT / raw_ref
|
||||||
assert ref_path.exists(), f"Observed timeline item [{index}] points to nonexistent capture_ref: {ref_path}"
|
assert ref_path.exists(), f"Observed timeline item [{index}] points to nonexistent capture_ref: {ref_path}"
|
||||||
assert ref_path.suffix == ".json", f"capture_ref must point to a JSON evidence record, got {ref_path}"
|
assert ref_path.suffix == ".json", f"capture_ref must point to a JSON evidence record, got {ref_path}"
|
||||||
|
|
||||||
|
# Confinement: evidence may only come from the native Herdr evidence root (no traversal, no foreign files).
|
||||||
|
resolved = ref_path.resolve()
|
||||||
|
try:
|
||||||
|
resolved.relative_to(HERDR_ROOT.resolve())
|
||||||
|
except ValueError:
|
||||||
|
raise AssertionError(
|
||||||
|
f"Observed timeline item [{index}] capture_ref '{raw_ref}' resolves outside the native Herdr "
|
||||||
|
f"evidence root {HERDR_ROOT}; foreign or traversed paths are not admissible evidence"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ref_data = json.loads(ref_path.read_text(encoding="utf-8"))
|
ref_data = json.loads(ref_path.read_text(encoding="utf-8"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -133,12 +158,12 @@ def _extract_native_agent_object(ref_data: dict):
|
|||||||
return agent
|
return agent
|
||||||
|
|
||||||
|
|
||||||
def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
def _validate_observed_herdr_item(index: int, item: dict, dctx: dict, scenario_backend: str = None) -> str:
|
||||||
"""Strict evidence correspondence for an observed Herdr timeline item.
|
"""Strict evidence correspondence for an observed Herdr timeline item. Returns the capture_id.
|
||||||
|
|
||||||
Every declared payload field must be a native field of the referenced capture's agent object
|
Every declared payload field must be a native field of the referenced capture's agent object
|
||||||
and must match its value exactly. Mandatory fields cannot be omitted to dodge the comparison,
|
and must match its value exactly. Mandatory fields cannot be omitted to dodge the comparison,
|
||||||
and the capture's agent session identity must match the scenario's dispatch attribution.
|
and the capture's identity (session UUID, pane id, backend) must match the scenario's attribution.
|
||||||
"""
|
"""
|
||||||
ref_path, ref_data = _load_observed_capture_ref(index, item)
|
ref_path, ref_data = _load_observed_capture_ref(index, item)
|
||||||
|
|
||||||
@@ -164,7 +189,7 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
|||||||
f"it cannot substantiate observed herdr timeline item [{index}]"
|
f"it cannot substantiate observed herdr timeline item [{index}]"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Identity correspondence: the capture must be about the session this scenario attributes.
|
# Identity correspondence (session): the capture must be about the session this scenario attributes.
|
||||||
session_obj = agent_obj.get("agent_session")
|
session_obj = agent_obj.get("agent_session")
|
||||||
capture_uuid = session_obj.get("value") if isinstance(session_obj, dict) else None
|
capture_uuid = session_obj.get("value") if isinstance(session_obj, dict) else None
|
||||||
assert capture_uuid == dctx["agent_session_uuid"], (
|
assert capture_uuid == dctx["agent_session_uuid"], (
|
||||||
@@ -172,6 +197,25 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
|||||||
f"'{dctx['agent_session_uuid']}' but capture {ref_path} records agent_session.value='{capture_uuid}'"
|
f"'{dctx['agent_session_uuid']}' but capture {ref_path} records agent_session.value='{capture_uuid}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Identity correspondence (pane): pane_process_generation is '<pane_id>:<pid>:<start>'; the capture's
|
||||||
|
# native pane_id must be its leading component, otherwise the capture is about a different pane/generation.
|
||||||
|
capture_pane = agent_obj.get("pane_id")
|
||||||
|
assert isinstance(capture_pane, str) and capture_pane, (
|
||||||
|
f"capture {ref_path} lacks a native pane_id; pane identity for timeline item [{index}] cannot be established"
|
||||||
|
)
|
||||||
|
ppg = str(dctx["pane_process_generation"])
|
||||||
|
assert ppg.startswith(capture_pane + ":"), (
|
||||||
|
f"Identity correspondence mismatch: dispatch_context.pane_process_generation='{ppg}' does not belong to "
|
||||||
|
f"pane_id='{capture_pane}' recorded by capture {ref_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Backend correspondence: a backend-specific scenario may only cite captures of that backend.
|
||||||
|
if scenario_backend and scenario_backend != BACKEND_AGNOSTIC:
|
||||||
|
assert ref_data.get("backend") == scenario_backend, (
|
||||||
|
f"Backend correspondence mismatch: scenario declares backend='{scenario_backend}' but capture "
|
||||||
|
f"{ref_path} records backend='{ref_data.get('backend')}'"
|
||||||
|
)
|
||||||
|
|
||||||
# Payload correspondence: every declared field must exist natively and match exactly.
|
# Payload correspondence: every declared field must exist natively and match exactly.
|
||||||
for key, declared in item_payload.items():
|
for key, declared in item_payload.items():
|
||||||
assert key in agent_obj, (
|
assert key in agent_obj, (
|
||||||
@@ -183,6 +227,55 @@ def _validate_observed_herdr_item(index: int, item: dict, dctx: dict) -> None:
|
|||||||
f"but capture {ref_path} records {key}='{agent_obj[key]}'"
|
f"but capture {ref_path} records {key}='{agent_obj[key]}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return str(ref_data["capture_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_watcher_manifest(manifest: dict) -> None:
|
||||||
|
"""The watcher manifest must not drift from the scenario files it indexes."""
|
||||||
|
for sid, sinfo in manifest["scenarios"].items():
|
||||||
|
data = json.loads((WATCHER_ROOT / sinfo["file"]).read_text(encoding="utf-8"))
|
||||||
|
assert data["scenario_id"] == sid, f"{sinfo['file']} scenario_id '{data['scenario_id']}' != manifest key '{sid}'"
|
||||||
|
assert data["provenance"] == sinfo["provenance"], (
|
||||||
|
f"{sid}: manifest provenance '{sinfo['provenance']}' != file provenance '{data['provenance']}'"
|
||||||
|
)
|
||||||
|
assert data["expected_policy"]["decision"] == sinfo["expected_decision"], (
|
||||||
|
f"{sid}: manifest expected_decision '{sinfo['expected_decision']}' != "
|
||||||
|
f"file decision '{data['expected_policy']['decision']}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_herdr_manifest(manifest: dict) -> None:
|
||||||
|
"""Every observed slot must be backed by a capture of the same backend recording the slot's state,
|
||||||
|
and every capture on disk must be indexed by exactly one observed slot (no drift, no orphans)."""
|
||||||
|
referenced = {}
|
||||||
|
for backend, bdata in manifest["backends"].items():
|
||||||
|
for slot_name, slot in bdata["slots"].items():
|
||||||
|
if slot["status"] != "observed":
|
||||||
|
continue
|
||||||
|
rel = slot["file"]
|
||||||
|
assert rel not in referenced, f"capture {rel} indexed by two slots: {referenced[rel]} and {backend}/{slot_name}"
|
||||||
|
referenced[rel] = f"{backend}/{slot_name}"
|
||||||
|
cap_path = HERDR_ROOT / rel
|
||||||
|
assert cap_path.exists(), f"Observed slot {backend}/{slot_name} references missing capture {rel}"
|
||||||
|
cap = json.loads(cap_path.read_text(encoding="utf-8"))
|
||||||
|
assert cap["backend"] == backend, f"{rel}: capture backend '{cap['backend']}' != manifest backend '{backend}'"
|
||||||
|
agent_obj = _extract_native_agent_object(cap)
|
||||||
|
if slot_name in HERDR_AGENT_STATUS_SLOTS:
|
||||||
|
assert agent_obj is not None, f"{rel}: status slot '{slot_name}' requires an agent object"
|
||||||
|
assert agent_obj.get("agent_status") == slot_name, (
|
||||||
|
f"{rel}: manifest slot '{slot_name}' but capture records agent_status='{agent_obj.get('agent_status')}'"
|
||||||
|
)
|
||||||
|
elif slot_name == "wait_timeout":
|
||||||
|
assert cap["exit_code"] != 0 and agent_obj is None, f"{rel}: wait_timeout slot must be a nonzero-exit capture"
|
||||||
|
err = json.loads(cap["stderr"]).get("error", {})
|
||||||
|
assert err.get("code") == "timeout", f"{rel}: wait_timeout slot must record error.code='timeout'"
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"Unknown observed slot kind '{slot_name}' for backend '{backend}'")
|
||||||
|
|
||||||
|
on_disk = {str(pth.relative_to(HERDR_ROOT)) for pth in HERDR_ROOT.glob("*/*.json")}
|
||||||
|
orphans = on_disk - set(referenced)
|
||||||
|
assert not orphans, f"Captures on disk not indexed by any observed manifest slot: {sorted(orphans)}"
|
||||||
|
|
||||||
|
|
||||||
def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
||||||
"""Strictly validates a Watcher composite scenario, attribution, timeline authenticity, and correspondence."""
|
"""Strictly validates a Watcher composite scenario, attribution, timeline authenticity, and correspondence."""
|
||||||
@@ -222,6 +315,8 @@ def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
|||||||
simulated_count = 0
|
simulated_count = 0
|
||||||
total_items = len(data["timeline"])
|
total_items = len(data["timeline"])
|
||||||
assert total_items > 0, "timeline must contain at least one item"
|
assert total_items > 0, "timeline must contain at least one item"
|
||||||
|
scenario_backend = data.get("backend")
|
||||||
|
cited_capture_ids: dict = {} # capture_id -> first timeline index that cited it
|
||||||
|
|
||||||
for i, item in enumerate(data["timeline"]):
|
for i, item in enumerate(data["timeline"]):
|
||||||
source_kind = item.get("source_kind")
|
source_kind = item.get("source_kind")
|
||||||
@@ -247,10 +342,17 @@ def validate_watcher_scenario(data: dict, file_path: Path = None) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if source_kind == "herdr":
|
if source_kind == "herdr":
|
||||||
_validate_observed_herdr_item(i, item, dctx)
|
capture_id = _validate_observed_herdr_item(i, item, dctx, scenario_backend)
|
||||||
else: # pragma: no cover - guarded by the assertion above
|
else: # pragma: no cover - guarded by the assertion above
|
||||||
raise AssertionError(f"Unhandled observed source_kind '{source_kind}' at timeline item [{i}]")
|
raise AssertionError(f"Unhandled observed source_kind '{source_kind}' at timeline item [{i}]")
|
||||||
|
|
||||||
|
# One capture is one point-in-time observation; it cannot substantiate two timeline points.
|
||||||
|
assert capture_id not in cited_capture_ids, (
|
||||||
|
f"Timeline item [{i}] re-cites capture_id '{capture_id}' already used by item "
|
||||||
|
f"[{cited_capture_ids[capture_id]}]; a single capture cannot substantiate multiple observations"
|
||||||
|
)
|
||||||
|
cited_capture_ids[capture_id] = i
|
||||||
|
|
||||||
# Provenance honesty invariant
|
# Provenance honesty invariant
|
||||||
if observed_count > 0 and simulated_count > 0:
|
if observed_count > 0 and simulated_count > 0:
|
||||||
assert overall_prov == "mixed", (
|
assert overall_prov == "mixed", (
|
||||||
@@ -636,11 +738,11 @@ def test_negative_observed_herdr_item_session_identity_mismatch_fails():
|
|||||||
|
|
||||||
|
|
||||||
def test_negative_observed_herdr_item_referencing_non_native_json_record_fails():
|
def test_negative_observed_herdr_item_referencing_non_native_json_record_fails():
|
||||||
"""A JSON file that is not a native Herdr capture (e.g. a Watcher scenario) is not Herdr evidence."""
|
"""A JSON file inside the evidence root that is not a native capture (e.g. the manifest) is not evidence."""
|
||||||
sc = _load_scenario("initial_idle.json")
|
sc = _load_scenario("initial_idle.json")
|
||||||
sc["provenance"] = "mixed"
|
sc["provenance"] = "mixed"
|
||||||
sc["timeline"][1]["source_provenance"] = "observed"
|
sc["timeline"][1]["source_provenance"] = "observed"
|
||||||
sc["timeline"][1]["capture_ref"] = "tests/fixtures/issue_5/watcher/finalized_candidate.json"
|
sc["timeline"][1]["capture_ref"] = "tests/fixtures/issue_5/herdr/manifest.json"
|
||||||
sc["timeline"][1]["payload"] = {"agent_status": "idle"}
|
sc["timeline"][1]["payload"] = {"agent_status": "idle"}
|
||||||
with pytest.raises(AssertionError, match="must have provenance 'observed'"):
|
with pytest.raises(AssertionError, match="must have provenance 'observed'"):
|
||||||
validate_watcher_scenario(sc)
|
validate_watcher_scenario(sc)
|
||||||
@@ -665,3 +767,124 @@ def test_positive_observed_herdr_item_with_full_native_correspondence_passes():
|
|||||||
sc["provenance"] = "observed_composite"
|
sc["provenance"] = "observed_composite"
|
||||||
with pytest.raises(AssertionError, match="overall provenance must be 'mixed'"):
|
with pytest.raises(AssertionError, match="overall provenance must be 'mixed'"):
|
||||||
validate_watcher_scenario(sc)
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# 6. Evidence Identity & Manifest Drift Tests (Rev.5 self-review hardening)
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
def _observed_herdr_scenario(payload: dict, capture_ref: str = HERDR_CLAUDE_IDLE) -> dict:
|
||||||
|
"""initial_idle with its herdr item (index 1) relabeled observed against `capture_ref`."""
|
||||||
|
sc = _load_scenario("initial_idle.json")
|
||||||
|
sc["provenance"] = "mixed"
|
||||||
|
sc["timeline"][1]["source_provenance"] = "observed"
|
||||||
|
sc["timeline"][1]["capture_ref"] = capture_ref
|
||||||
|
sc["timeline"][1]["payload"] = payload
|
||||||
|
return sc
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad_ref, expected", [
|
||||||
|
# Escapes the evidence root via traversal even though the target is a well-formed JSON file.
|
||||||
|
("tests/fixtures/issue_5/herdr/../watcher/finalized_candidate.json", "resolves outside the native Herdr evidence root"),
|
||||||
|
# Well-formed JSON in the repo but not under the evidence root.
|
||||||
|
("tests/fixtures/issue_5/watcher/finalized_candidate.json", "resolves outside the native Herdr evidence root"),
|
||||||
|
# Absolute paths are never admissible, even when they point at a real capture.
|
||||||
|
(str(HERDR_ROOT / "claude" / "idle.json"), "must be a repo-relative path"),
|
||||||
|
])
|
||||||
|
def test_negative_observed_capture_ref_outside_evidence_root_fails(bad_ref, expected):
|
||||||
|
sc = _observed_herdr_scenario({"agent_status": "idle"}, capture_ref=bad_ref)
|
||||||
|
with pytest.raises(AssertionError, match=expected):
|
||||||
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_single_capture_cited_by_two_observed_items_fails():
|
||||||
|
"""One point-in-time capture cannot substantiate two distinct timeline observations."""
|
||||||
|
sc = _load_scenario("initial_idle.json")
|
||||||
|
sc["provenance"] = "observed_composite"
|
||||||
|
# Turn the event item into a second herdr observation citing the very same capture.
|
||||||
|
for item in sc["timeline"]:
|
||||||
|
item["source_kind"] = "herdr"
|
||||||
|
item["source_provenance"] = "observed"
|
||||||
|
item["capture_ref"] = HERDR_CLAUDE_IDLE
|
||||||
|
item["payload"] = {"agent_status": "idle"}
|
||||||
|
with pytest.raises(AssertionError, match="re-cites capture_id 'cap-claude-idle-01'"):
|
||||||
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_observed_herdr_item_pane_identity_mismatch_fails():
|
||||||
|
"""Session UUID may match while the pane generation belongs to a different pane; must be rejected."""
|
||||||
|
sc = _observed_herdr_scenario({"agent_status": "idle"})
|
||||||
|
sc["dispatch_context"]["pane_process_generation"] = "w2X:p9:1234:2026-09-06T00:00:00Z" # capture says w2X:p1
|
||||||
|
with pytest.raises(AssertionError, match="does not belong to pane_id='w2X:p1'"):
|
||||||
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
# A pane id that merely shares a prefix (w2X:p1 vs w2X:p10) must not be accepted either.
|
||||||
|
sc["dispatch_context"]["pane_process_generation"] = "w2X:p10:1234:2026-09-06T00:00:00Z"
|
||||||
|
with pytest.raises(AssertionError, match="does not belong to pane_id='w2X:p1'"):
|
||||||
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_backend_specific_scenario_citing_other_backend_capture_fails():
|
||||||
|
"""A scenario pinned to one backend may not borrow another backend's capture as evidence."""
|
||||||
|
sc = _observed_herdr_scenario({"agent_status": "idle"})
|
||||||
|
sc["backend"] = "grok" # capture is claude
|
||||||
|
with pytest.raises(AssertionError, match="Backend correspondence mismatch"):
|
||||||
|
validate_watcher_scenario(sc)
|
||||||
|
|
||||||
|
sc["backend"] = "claude"
|
||||||
|
validate_watcher_scenario(sc) # matching backend is accepted
|
||||||
|
|
||||||
|
sc["backend"] = BACKEND_AGNOSTIC
|
||||||
|
validate_watcher_scenario(sc) # agnostic scenarios may cite any backend
|
||||||
|
|
||||||
|
|
||||||
|
def test_watcher_manifest_agrees_with_scenario_files():
|
||||||
|
validate_watcher_manifest(json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8")))
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_watcher_manifest_drift_is_detected():
|
||||||
|
manifest = json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
manifest["scenarios"]["initial_idle"]["expected_decision"] = "candidate_approved" # file says hold
|
||||||
|
with pytest.raises(AssertionError, match="manifest expected_decision 'candidate_approved' != file decision 'hold'"):
|
||||||
|
validate_watcher_manifest(manifest)
|
||||||
|
|
||||||
|
manifest = json.loads((WATCHER_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
manifest["scenarios"]["finalized_candidate"]["provenance"] = "observed_composite" # file says simulated
|
||||||
|
with pytest.raises(AssertionError, match="manifest provenance 'observed_composite' != file provenance 'simulated'"):
|
||||||
|
validate_watcher_manifest(manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def test_herdr_manifest_observed_slots_agree_with_captures_and_no_orphan_captures():
|
||||||
|
validate_herdr_manifest(json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8")))
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_herdr_manifest_slot_status_drift_is_detected():
|
||||||
|
"""Re-indexing claude/idle.json (records idle) under the 'done' slot must be rejected."""
|
||||||
|
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
slots = manifest["backends"]["claude"]["slots"]
|
||||||
|
slots["done"] = {"status": "observed", "file": "claude/idle.json"}
|
||||||
|
slots["idle"] = {"status": "unobserved", "reason": "mutated"}
|
||||||
|
with pytest.raises(AssertionError, match="manifest slot 'done' but capture records agent_status='idle'"):
|
||||||
|
validate_herdr_manifest(manifest)
|
||||||
|
|
||||||
|
|
||||||
|
def test_negative_herdr_manifest_backend_or_orphan_drift_is_detected():
|
||||||
|
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
# Borrow another backend's capture: grok's idle slot pointed at claude's capture (claude's own slot
|
||||||
|
# de-indexed so the duplicate-index guard does not fire first and the backend check is what rejects).
|
||||||
|
manifest["backends"]["claude"]["slots"]["idle"] = {"status": "unobserved", "reason": "mutated"}
|
||||||
|
manifest["backends"]["grok"]["slots"]["idle"]["file"] = "claude/idle.json"
|
||||||
|
with pytest.raises(AssertionError, match="capture backend 'claude' != manifest backend 'grok'"):
|
||||||
|
validate_herdr_manifest(manifest)
|
||||||
|
|
||||||
|
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
# Same capture indexed by two slots must be rejected.
|
||||||
|
manifest["backends"]["grok"]["slots"]["idle"]["file"] = "claude/idle.json"
|
||||||
|
with pytest.raises(AssertionError, match="indexed by two slots"):
|
||||||
|
validate_herdr_manifest(manifest)
|
||||||
|
|
||||||
|
manifest = json.loads((HERDR_ROOT / "manifest.json").read_text(encoding="utf-8"))
|
||||||
|
# De-index a real capture: it becomes an orphan on disk and must be reported.
|
||||||
|
manifest["backends"]["codex"]["slots"]["done"] = {"status": "unobserved", "reason": "mutated"}
|
||||||
|
with pytest.raises(AssertionError, match=r"not indexed by any observed manifest slot: \['codex/done.json'\]"):
|
||||||
|
validate_herdr_manifest(manifest)
|
||||||
Reference in new issue
Block a user