[verified] Document conditional pytest evidence summary recipe

This commit is contained in:
2026-10-12 15:19:46 +00:00
parent 14b27bb708
commit e5339694ab
2 changed files with 99 additions and 0 deletions
@@ -196,6 +196,19 @@ Reviewer가 rework head를 새 snapshot으로 다시 검토해야 하며 이 com
모든 baseline, 통합·precommit 및 readback 검증은 `scripts/evidence_manifest.py`와 `templates/evidence-plan.json`을 사용해 exact argv, exit code, 제한된 output, 허용된 환경, source revision, 파일 hash와 표준 JSON-pointer claims를 기록한다. 명령은 argv 배열만 허용하고 명시된 최소 child-environment allowlist에서 실행하며 output 상한을 넘는 process는 중단한다. Secret·header·private-key·password 값이나 plan 금지 패턴이 발견되면 redaction하지 않고 fail-closed한다. `run`은 baseline부터 최종 precommit까지 사용하고, `validate`는 제품 tree의 evidence exclusion 없이 strict clean checkout에서 PR claim 생성 전에 실행한다. 새 기록은 external verification root의 v2이며 exact current HEAD와 source blob·hash를 다시 검증한다. 과거 evidence-only descendant는 명시적 archived v1 read-only 호환 경로에 한정한다. 수동으로 매칭되지 않는 PR 성공·검증 주장을 만들지 않는다. Comment publish 전 remote immutable artifact의 digest를 authoritative readback으로 비교한다. 모든 baseline, 통합·precommit 및 readback 검증은 `scripts/evidence_manifest.py`와 `templates/evidence-plan.json`을 사용해 exact argv, exit code, 제한된 output, 허용된 환경, source revision, 파일 hash와 표준 JSON-pointer claims를 기록한다. 명령은 argv 배열만 허용하고 명시된 최소 child-environment allowlist에서 실행하며 output 상한을 넘는 process는 중단한다. Secret·header·private-key·password 값이나 plan 금지 패턴이 발견되면 redaction하지 않고 fail-closed한다. `run`은 baseline부터 최종 precommit까지 사용하고, `validate`는 제품 tree의 evidence exclusion 없이 strict clean checkout에서 PR claim 생성 전에 실행한다. 새 기록은 external verification root의 v2이며 exact current HEAD와 source blob·hash를 다시 검증한다. 과거 evidence-only descendant는 명시적 archived v1 read-only 호환 경로에 한정한다. 수동으로 매칭되지 않는 PR 성공·검증 주장을 만들지 않는다. Comment publish 전 remote immutable artifact의 digest를 authoritative readback으로 비교한다.
### Conditional pytest evidence recipe
pytest를 이미 사용하는 프로젝트의 evidence command에만 적용한다. unittest 예시나 제품의 pytest 설정을 일괄 변경하지 않고, 승인된 interpreter·pytest 버전·plugin 및 기존 test target을 사용한다. 아래 JSON argv의 `{python}`과 `{tests}`를 exact interpreter와 승인 target으로 치환한다. 여러 target은 각각 argv 원소로 명시하고 shell 문자열로 합치지 않는다.
```json
["{python}", "-B", "-m", "pytest", "-o", "addopts=", "--color=no", "-rA", "{tests}"]
```
- 이 recipe는 command-local `addopts=`로 프로젝트의 quiet 누적 옵션을 해제하여 최종 summary를 보존하는 예시다. 기본적으로 `PYTEST_ADDOPTS`는 기존 child-environment allowlist에 없으며 전달하지 않는다. Quiet 자체가 항상 summary를 누락시키는 원인이라고 단정하지 않는다. 프로젝트 `addopts`의 필수 실행·plugin 옵션은 사전에 확인하여 필요한 옵션을 승인된 argv에 명시적으로 옮긴다. 필수 semantics를 보존할 수 없으면 recipe를 그대로 적용하지 않고 `evidence_gap`으로 남긴다. Warning 필터·skip 조건·선택 대상·제품 config 파일은 변경하지 않는다.
- 실제 승인 환경에서 먼저 최종 passed/skipped/failed 및 warning 요약의 보존 여부를 확인한다. pytest가 생략하는 zero category를 출력된 수치처럼 보고하거나 임의로 0으로 채우지 않는다. 이전 실행의 수치나 progress 문자 개수로 이번 실행 합계를 보충하지 않는다. Recipe만으로 모든 pytest 버전·plugin의 output 보존을 보장하지 않는다.
- 기존 evidence plan의 finite `timeout_seconds`, `max_output_bytes`, expected exit code와 process cleanup을 유지하고 exact argv·환경·source SHA·현재 실행 output을 기록한다. `timeout`·`output_limit`·불완전 summary이면 수치 claim은 `evidence_gap`이다. 명령이 완료되고 exit 0인 사실과 정확한 수치 보고의 증거는 구분한다. 승인 없이 재실행하거나 output 한도를 늘리지 않는다.
- 별도의 parser, runner, manifest schema 또는 pytest 설치 의무를 추가하지 않는다. Synthetic regression은 격리된 pytest interpreter에서 실제 pass/skip/warning과 failure 출력 및 기존 bounded reader의 보존을 검증한다. Pytest가 없는 test interpreter에서는 실행 회귀가 명시적으로 skip되므로 문서 계약 테스트만 통과한 결과를 실제 pytest 검증으로 보고하지 않는다. Synthetic test의 plugin autoload 차단은 fixture 격리용이며 제품 실행 정책이 아니다.
### 1. Target, identity와 review 고정 ### 1. Target, identity와 review 고정
1. Exact target identifier를 검증하고 broker `serve`의 ready 응답과 public `request` API로 Issue lease를 확인한다. 1. Exact target identifier를 검증하고 broker `serve`의 ready 응답과 public `request` API로 Issue lease를 확인한다.
@@ -0,0 +1,86 @@
"""Conditional pytest recipe: documentation contract and real synthetic execution.
Run with an independently admitted pytest interpreter for execution coverage.
No dependency installation occurs in this test or the production workflow.
"""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[3]
SKILL = ROOT / 'harness/agents/impl/skills/gitea-issue-rework/SKILL.md'
HEADING = '### Conditional pytest evidence recipe'
class PytestEvidenceRecipeTests(unittest.TestCase):
def recipe(self):
text = SKILL.read_text()
self.assertIn(HEADING, text, 'conditional pytest recipe is missing')
section = text.split(HEADING, 1)[1].split('\n### ', 1)[0]
return section, json.loads(section.split('```json\n', 1)[1].split('\n```', 1)[0])
def test_documentation_contract_limits_recipe_to_existing_pytest_projects(self):
section, argv = self.recipe()
for rule in ['pytest를 이미 사용하는', 'PYTEST_ADDOPTS', 'plugin',
'evidence_gap', '이전 실행', '0으로', 'schema', 'unittest',
'warning', 'timeout', 'output_limit']:
self.assertIn(rule, section)
self.assertIsInstance(argv, list)
self.assertEqual(argv[:4], ['{python}', '-B', '-m', 'pytest'])
@unittest.skipUnless(importlib.util.find_spec('pytest'), 'admitted pytest interpreter required')
def test_real_recipe_preserves_pass_skip_warning_summary_under_quiet_project_config(self):
_, argv = self.recipe()
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / 'pytest.ini').write_text('[pytest]\naddopts = -qq\n')
(root / 'test_sample.py').write_text(
'import pytest, warnings\n'
'def test_pass():\n warnings.warn("synthetic warning", UserWarning)\n'
'@pytest.mark.skip(reason="synthetic skip")\ndef test_skip():\n pass\n')
control = self.invoke([sys.executable, '-B', '-m', 'pytest', 'test_sample.py'], root)
self.assertEqual(control['status'], 'completed')
self.assertEqual(control['exit_code'], 0)
self.assertNotIn('1 passed', control['output'])
actual = self.invoke(self.expand(argv), root)
self.assertEqual(actual['status'], 'completed')
self.assertEqual(actual['exit_code'], 0)
self.assertRegex(actual['output'], r'1 passed, 1 skipped, 1 warning')
self.assertNotIn('\x1b[', actual['output'])
@unittest.skipUnless(importlib.util.find_spec('pytest'), 'admitted pytest interpreter required')
def test_real_recipe_preserves_failure_summary_and_exit_code(self):
_, argv = self.recipe()
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / 'pytest.ini').write_text('[pytest]\naddopts = -qq\n')
(root / 'test_sample.py').write_text('def test_fail():\n assert False, "synthetic failure"\n')
actual = self.invoke(self.expand(argv), root)
self.assertEqual(actual['status'], 'completed')
self.assertEqual(actual['exit_code'], 1)
self.assertIn('1 failed', actual['output'])
def expand(self, argv):
return [part.replace('{python}', sys.executable).replace('{tests}', 'test_sample.py') for part in argv]
def invoke(self, argv, root):
import importlib.util
path = ROOT / 'harness/agents/impl/skills/gitea-issue-rework/scripts/evidence_manifest.py'
spec = importlib.util.spec_from_file_location('pytest_recipe_evidence', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
env = {key: os.environ[key] for key in ['PATH', 'HOME', 'TMPDIR', 'LANG'] if key in os.environ}
# Isolate synthetic tests from unrelated globally installed pytest plugins.
env['PYTEST_DISABLE_PLUGIN_AUTOLOAD'] = '1'
result = module.run_bounded(argv, root, env, 65536, 30)
return {'status': result['status'], 'exit_code': result['exit_code'],
'output': result['output'].decode('utf-8', 'replace')}
if __name__ == '__main__':
unittest.main()