mirror of
https://github.com/tradecatlabs/vibe-coding-cn.git
synced 2026-11-02 12:16:13 +00:00
534 lines
33 KiB
Python
534 lines
33 KiB
Python
#!/usr/bin/env python3
|
|
"""功法JSON协议的CLI集成测试;仅使用隔离文件,不联网、不实际评级。
|
|
|
|
运行:python3 .github/test-gongfa.py --artifacts /tmp/gongfa-test-run
|
|
产物:每个输入与CLI输出、result.json和JUnit XML。需要功法检查器的依赖。
|
|
"""
|
|
|
|
import argparse
|
|
import copy
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import source_archive
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ARTIFACTS = None
|
|
EXPECTED_INITIAL_IDS = {
|
|
'gongfa-compositional-description', 'gongfa-network-effects', 'gongfa-occams-razor',
|
|
'gongfa-glue-principle', 'gongfa-first-principles', 'gongfa-abductive-debugging',
|
|
}
|
|
# 清单来自收录范围,不调用被测提取器产生期望。
|
|
EXPECTED_WEBSITE_NAMES = {
|
|
'固定点收敛系统', '递归式 AI 治理', '拼好码', '吸引子', '有限样本生成推断',
|
|
'AI-native', '拍扁', '符号序列空间与有效知识子集', '合成模型', '解空间',
|
|
'近端因果机制', '目的导向的行动选择', '预测加工与模型更新', '受约束的状态变换',
|
|
'最小先验条件下的代理增强型任务求解', 'Projection 表示转换', '状态分类工具',
|
|
}
|
|
# 本轮清单由已读独立方法及真实锚点确定,不从registry反向生成。
|
|
EXPECTED_EXPANSION_ANCHORS = {
|
|
'gongfa-top-down-system-design': 'concept-system-building-一自顶向下先看整体再拆细节',
|
|
'gongfa-bottom-up-system-building': 'concept-system-building-二自底向上先做组件再组系统',
|
|
'gongfa-divide-and-conquer': 'concept-system-building-三分而治之把复杂问题拆成小问题',
|
|
'gongfa-problem-definition-framework': 'concept-problem-solving-5-搭建体系',
|
|
'gongfa-methodology-delivery-loop': 'philosophy-methodology-toolbox-总体作业流',
|
|
'gongfa-cybernetic-feedback-method': 'philosophy-methodology-toolbox-控制论与科学方法论',
|
|
'gongfa-recursive-generator-optimization': 'concept-recursive-self-optimizing-system',
|
|
'gongfa-specification-before-implementation': 'philosophy-programming-dao-规约先于实现',
|
|
'gongfa-state-ownership-questions': 'philosophy-programming-dao-10-时间视角',
|
|
}
|
|
# 仓内续收范围来自逐节阅读,不以文件数或标题数充当独立功法数。
|
|
EXPECTED_REPO_CONTINUATION_ANCHORS = {
|
|
'gongfa-high-cohesion': 'philosophy-programming-dao-高内聚',
|
|
'gongfa-low-coupling': 'philosophy-programming-dao-低耦合',
|
|
'gongfa-stable-interface-contract': 'philosophy-programming-dao-稳定接口不稳定实现',
|
|
'gongfa-explicit-dependencies': 'philosophy-programming-dao-隐藏的依赖最危险',
|
|
'gongfa-invariant-constraints': 'philosophy-programming-dao-不变式保持世界稳定',
|
|
'gongfa-reasonable-code': 'philosophy-programming-dao-9-可推理性',
|
|
'gongfa-least-surprise': 'philosophy-programming-dao-16-最小惊讶原则',
|
|
'gongfa-validated-abstraction': 'philosophy-software-engineering-truths-5-重复代码不一定坏错误抽象更坏',
|
|
'gongfa-technical-debt-ledger': 'philosophy-software-engineering-truths-14-技术债不是罪假装没有技术债才是罪',
|
|
'gongfa-performance-by-measurement': 'philosophy-software-engineering-truths-16-性能问题要靠测量不要靠感觉',
|
|
'gongfa-controllable-failure': 'philosophy-software-engineering-truths-18-稳定不是没有故障而是故障可控',
|
|
'gongfa-decision-rationale-documentation': 'philosophy-software-engineering-truths-23-文档不是装饰品',
|
|
'gongfa-dataset-first-architecture': 'reference-engineering-practice-7-dataset-first-数据服务结构',
|
|
'gongfa-dataset-service-construction': 'reference-engineering-practice-新建数据服务流程',
|
|
'gongfa-external-source-dataset-adaptation': 'reference-engineering-practice-外部源码接入流程',
|
|
'gongfa-symbol-name-index-maintenance': 'reference-engineering-practice-1-变量名维护方案',
|
|
'gongfa-input-state-transform-separation': 'reference-engineering-practice-33-消费端-生产端-状态变量-变换函数',
|
|
'gongfa-shared-resource-concurrency': 'reference-engineering-practice-34-并发concurrency',
|
|
'gongfa-dry-reuse': 'reference-engineering-practice-54-dry-原则不要重复',
|
|
'gongfa-input-scale-complexity-constraints': 'reference-engineering-practice-三新手常见复杂度误区',
|
|
'gongfa-cache-lifecycle-isolation': 'reference-engineering-practice-六缓存误用',
|
|
}
|
|
|
|
|
|
class GongfaProtocolTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.fixture = ARTIFACTS / 'fixture'
|
|
source_archive.export_documents(ROOT, 'gongfa', cls.fixture)
|
|
cls.baseline = json.loads((cls.fixture / '.github/metadata/gongfa/registry.json').read_text())
|
|
cls.sequence = 0
|
|
|
|
def run_catalog(self, document, label, extra_args=()):
|
|
type(self).sequence += 1
|
|
stem = ARTIFACTS / ('%03d-%s' % (self.sequence, label))
|
|
path = stem.with_suffix('.json')
|
|
path.write_text(json.dumps(document, ensure_ascii=False, allow_nan=False) + '\n')
|
|
return self.run_path(path, stem, extra_args)
|
|
|
|
def run_path(self, path, stem, extra_args=()):
|
|
command = [sys.executable, str(ROOT / '.github/check-gongfa.py'),
|
|
'--root', str(self.fixture), '--registry', str(path)] + list(extra_args)
|
|
process = subprocess.run(command, capture_output=True, text=True, timeout=20)
|
|
stem.with_suffix('.log').write_text(process.stdout + process.stderr)
|
|
stem.with_suffix('.command.json').write_text(json.dumps({
|
|
'command': command, 'returncode': process.returncode,
|
|
'input_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
}, ensure_ascii=False, indent=2) + '\n')
|
|
return process
|
|
|
|
def test_complete_initial_collection(self):
|
|
process = self.run_catalog(self.baseline, 'initial-collection')
|
|
self.assertEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
summary = json.loads(process.stdout)
|
|
self.assertEqual(summary['grade_values'], 12)
|
|
self.assertGreaterEqual(summary['content_versions'], 49)
|
|
self.assertEqual(summary['rated_assessments'], 0)
|
|
entries = self.baseline['gongfa']
|
|
for identity in EXPECTED_INITIAL_IDS:
|
|
matches = [entry for entry in entries if entry['id'] == identity]
|
|
self.assertEqual(len(matches), 1)
|
|
self.assertEqual(matches[0]['content_version'], 'r1')
|
|
self.assertEqual(matches[0]['entry_status'], 'registered')
|
|
site = [entry for entry in entries if entry['source_ref']['source_id'] ==
|
|
'tradecatlabs-vibe-coding-methodology-page']
|
|
self.assertEqual({entry['name'] for entry in site}, EXPECTED_WEBSITE_NAMES)
|
|
by_id = {entry['id']: entry for entry in entries}
|
|
self.assertEqual(by_id['gongfa-ai-native']['content']['profile']['questions'], [
|
|
'更一般地,它追问机器自主性上升后,行动权、控制权与最终责任如何重新分配。'])
|
|
self.assertTrue(any('持续操控' in item for item in
|
|
by_id['gongfa-predictive-processing-update']['content']['profile']['limitations']))
|
|
self.assertTrue(any('MRE' in item for item in
|
|
by_id['gongfa-phenomenological-reduction']['content']['profile']['expected_outcomes']))
|
|
self.assertIsNotNone(by_id['gongfa-state-space-thinking']['content']['profile']['problem'])
|
|
for identity, boundary in [
|
|
('gongfa-fixed-point-convergence', '缺乏稳定性'),
|
|
('gongfa-attractor-model', '指标投机'),
|
|
('gongfa-minimal-prior-agent-assisted-solving', '操作风险')]:
|
|
with self.subTest(identity=identity):
|
|
self.assertTrue(any(boundary in item for item in
|
|
by_id[identity]['content']['profile']['limitations']))
|
|
self.assertTrue(any('人工介入' in item for item in
|
|
by_id['gongfa-recursive-ai-governance']['content']['profile']['expected_outcomes']))
|
|
for identity, anchor in EXPECTED_EXPANSION_ANCHORS.items():
|
|
with self.subTest(identity=identity):
|
|
self.assertIn(identity, set(by_id))
|
|
entry = by_id[identity]
|
|
self.assertEqual(entry['source_ref']['heading_id'], anchor)
|
|
self.assertEqual(entry['entry_status'], 'registered')
|
|
self.assertEqual(entry['content_version'], 'r1')
|
|
self.assertIn(entry['ontology_type'], {
|
|
'explanatory-content', 'normative-allocation-content',
|
|
'method-specification', 'dao-content'})
|
|
for identity, field, phrase in [
|
|
('gongfa-compositional-description', 'questions', '同一个'),
|
|
('gongfa-glue-principle', 'questions', '官方能力'),
|
|
('gongfa-first-principles', 'questions', '前提'),
|
|
('gongfa-phenomenological-reduction', 'conditions', '复现'),
|
|
('gongfa-top-down-system-design', 'limitations', '返工'),
|
|
('gongfa-bottom-up-system-building', 'limitations', '拼不起来'),
|
|
('gongfa-divide-and-conquer', 'limitations', '集成'),
|
|
('gongfa-recursive-generator-optimization', 'conditions', '压缩性')]:
|
|
with self.subTest(identity=identity, field=field):
|
|
self.assertIn(identity, set(by_id))
|
|
self.assertTrue(any(phrase in item for item in by_id[identity]['content']['profile'][field]))
|
|
# 网络效应r1只收一句解释,不得借同文章后面的操作版补造条件。
|
|
self.assertEqual(by_id['gongfa-network-effects']['content']['profile']['conditions'], [])
|
|
self.assertEqual(by_id['gongfa-network-effects']['content']['profile']['limitations'], [])
|
|
causal = by_id['gongfa-proximate-causal-mechanism']['content']['profile']
|
|
self.assertTrue(all('若存在' not in item for item in causal['conditions']))
|
|
self.assertTrue(any('共同原因' in item for item in causal['limitations']))
|
|
self.assertTrue(all(entry['ontology_type'] is not None for entry in entries))
|
|
self.assertTrue(all(entry['entry_status'] == 'registered' for entry in entries))
|
|
proposals = self.baseline.get('grade_proposals', [])
|
|
self.assertTrue(proposals, '应有实际人工初评批次,而非只保留未评级条目')
|
|
targets = {(entry['id'], entry['content_version']) for entry in entries}
|
|
self.assertEqual({(item['gongfa_id'], item['content_version'])
|
|
for batch in proposals for item in batch['items']}, targets)
|
|
self.assertEqual(summary['proposed_grades'], len(entries))
|
|
for batch in proposals:
|
|
if batch['policy_id'] is None:
|
|
self.assertIsNone(batch['policy_version'])
|
|
self.assertEqual(batch['basis'], 'provisional_expert_judgment')
|
|
else:
|
|
self.assertEqual(batch['basis'], 'expert_judgment')
|
|
self.assertEqual(batch['status'], 'provisional')
|
|
|
|
def test_repository_continuation(self):
|
|
by_id = {entry['id']: entry for entry in self.baseline['gongfa']}
|
|
for identity, anchor in EXPECTED_REPO_CONTINUATION_ANCHORS.items():
|
|
with self.subTest(identity=identity):
|
|
self.assertIn(identity, by_id)
|
|
entry = by_id[identity]
|
|
self.assertEqual(entry['source_ref']['heading_id'], anchor)
|
|
self.assertEqual(entry['content_version'], 'r1')
|
|
self.assertEqual(entry['entry_status'], 'registered')
|
|
batches = [batch for batch in self.baseline['grade_proposals']
|
|
if batch['id'] == 'gongfa-author-repo-expansion-20261007']
|
|
self.assertEqual(len(batches), 1)
|
|
self.assertIsNone(batches[0]['supersedes'])
|
|
self.assertEqual({item['gongfa_id'] for item in batches[0]['items']},
|
|
set(EXPECTED_REPO_CONTINUATION_ANCHORS))
|
|
original = [batch for batch in self.baseline['grade_proposals']
|
|
if batch['id'] == 'gongfa-author-initial-20261007']
|
|
self.assertEqual(len(original), 1)
|
|
self.assertTrue(set(EXPECTED_REPO_CONTINUATION_ANCHORS).isdisjoint(
|
|
item['gongfa_id'] for item in original[0]['items']))
|
|
prefix = by_id['gongfa-dataset-first-architecture']
|
|
self.assertEqual(prefix['source_ref']['selection'], 'markdown-prefix')
|
|
self.assertIn('不适合直接照抄', prefix['content']['original_text'])
|
|
self.assertNotIn('service-root/', prefix['content']['original_text'])
|
|
self.assertNotIn('新建数据服务流程', prefix['content']['original_text'])
|
|
self.assertEqual(by_id['gongfa-stable-interface-contract']['content']['original_text'],
|
|
'- API 是契约\n- 实现是细节\n- 不破坏契约,就是负责')
|
|
for identity in ('gongfa-high-cohesion', 'gongfa-stable-interface-contract',
|
|
'gongfa-input-scale-complexity-constraints'):
|
|
profile = by_id[identity]['content']['profile']
|
|
self.assertEqual(profile['conditions'], [])
|
|
self.assertIn('conditions', profile['missing_fields'])
|
|
|
|
def proposal_batch(self):
|
|
entry = self.baseline['gongfa'][0]
|
|
policy = self.baseline['policies'][0]
|
|
return {
|
|
'id': 'test-initial-grading', 'vocabulary_id': 'gongfa-grade-vocabulary-v1',
|
|
'policy_id': policy['id'], 'policy_version': policy['version'],
|
|
'status': 'provisional', 'basis': 'expert_judgment',
|
|
'assessor': '隔离协议测试,不是独立效果评审', 'assessed_at': '2026-10-07T12:00:00Z',
|
|
'application_context': '解释软件系统中对象、状态与变化的受控案例',
|
|
'baseline_assumption': '同目标和预算下采用现行需求、设计与验证流程',
|
|
'limits': ['人工初评不是实际效果或独立实验结论。'], 'supersedes': None,
|
|
'items': [{
|
|
'gongfa_id': entry['id'], 'content_version': entry['content_version'],
|
|
'content_sha256': entry['content']['text_sha256'],
|
|
'ontology_type': 'explanatory-content', 'grade_id': 'xuan-initial',
|
|
'task_family': '系统变化的解释与诊断',
|
|
'classification_reason': '限定内容解释对象、状态、过程及关系,不规定一次执行。',
|
|
'reason': '隔离测试中的人工建议,不宣称已取得基线增益。',
|
|
'confidence': 'low',
|
|
'falsifier': '若受控案例中不能识别对象同一性与变化,此建议应下调。',
|
|
}],
|
|
}
|
|
|
|
def test_provisional_grading_record_shape(self):
|
|
document = copy.deepcopy(self.baseline)
|
|
proposal = self.proposal_batch()
|
|
document['grade_proposals'] = [proposal]
|
|
process = self.run_catalog(document, 'valid-provisional-grading')
|
|
self.assertEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
self.assertEqual(json.loads(process.stdout)['proposed_grades'], 1)
|
|
table = self.run_catalog(document, 'render-provisional-catalog', ['--render-catalog'])
|
|
self.assertEqual(table.returncode, 0, table.stdout + table.stderr)
|
|
self.assertIn('| `gongfa-compositional-description` | 组合描述模型 | 解释型 | 玄阶初级 |', table.stdout)
|
|
self.assertIn('所选初评批次:`test-initial-grading`', table.stdout)
|
|
self.assertIn('| `gongfa-network-effects` | 网络效应 | 解释型 | 本批未初评 |', table.stdout)
|
|
for label in ('bad-grade', 'null-type', 'bad-type', 'unknown-target', 'unknown-policy',
|
|
'wrong-content-digest', 'duplicate-target', 'duplicate-batch',
|
|
'missing-task', 'missing-reason', 'missing-falsifier', 'fake-rated',
|
|
'fake-experiment', 'missing-history', 'history-cycle', 'history-scope'):
|
|
with self.subTest(label=label):
|
|
damaged = copy.deepcopy(document)
|
|
batch = damaged['grade_proposals'][0]
|
|
item = batch['items'][0]
|
|
if label == 'bad-grade': item['grade_id'] = 'sheng-initial'
|
|
elif label == 'null-type': item['ontology_type'] = None
|
|
elif label == 'bad-type': item['ontology_type'] = 'software-artifact'
|
|
elif label == 'unknown-target': item['gongfa_id'] = 'unknown-gongfa'
|
|
elif label == 'unknown-policy': batch['policy_id'] = 'unknown-policy'
|
|
elif label == 'wrong-content-digest': item['content_sha256'] = '0' * 64
|
|
elif label == 'duplicate-target': batch['items'].append(copy.deepcopy(item))
|
|
elif label == 'duplicate-batch': damaged['grade_proposals'].append(copy.deepcopy(batch))
|
|
elif label == 'missing-task': item['task_family'] = ''
|
|
elif label == 'missing-reason': item['reason'] = ''
|
|
elif label == 'missing-falsifier': item['falsifier'] = ''
|
|
elif label == 'fake-rated': batch['status'] = 'rated'
|
|
elif label == 'fake-experiment': batch['basis'] = 'experimental_result'
|
|
elif label == 'missing-history': batch['supersedes'] = 'missing-batch'
|
|
else:
|
|
previous = copy.deepcopy(batch)
|
|
previous['id'] = 'older-test-grading'
|
|
damaged['grade_proposals'].append(previous)
|
|
batch['supersedes'] = previous['id']
|
|
if label == 'history-cycle': previous['supersedes'] = batch['id']
|
|
else: previous['application_context'] = '另一应用范围'
|
|
process = self.run_catalog(damaged, label)
|
|
self.assertNotEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
repeated = copy.deepcopy(proposal)
|
|
repeated['id'] = 'corrected-test-grading'
|
|
repeated['supersedes'] = proposal['id']
|
|
document['grade_proposals'].append(repeated)
|
|
self.assertEqual(self.run_catalog(document, 'valid-grading-correction').returncode, 0)
|
|
self.assertNotEqual(self.run_catalog(document, 'ambiguous-catalog', ['--render-catalog']).returncode, 0)
|
|
explicit = self.run_catalog(document, 'explicit-catalog',
|
|
['--render-catalog', '--proposal-batch', proposal['id']])
|
|
self.assertEqual(explicit.returncode, 0, explicit.stdout + explicit.stderr)
|
|
|
|
def test_imported_provisional_opinion_can_preserve_missing_policy_binding(self):
|
|
document = copy.deepcopy(self.baseline)
|
|
proposal = self.proposal_batch()
|
|
proposal['policy_id'] = None
|
|
proposal['policy_version'] = None
|
|
proposal['basis'] = 'provisional_expert_judgment'
|
|
document['grade_proposals'] = [proposal]
|
|
accepted = self.run_catalog(document, 'unbound-source-opinion')
|
|
self.assertEqual(accepted.returncode, 0, accepted.stdout + accepted.stderr)
|
|
proposal['policy_version'] = 'v1'
|
|
rejected = self.run_catalog(document, 'partially-bound-source-opinion')
|
|
self.assertNotEqual(rejected.returncode, 0, rejected.stdout + rejected.stderr)
|
|
|
|
def test_undetermined_proposal_is_explicit_and_never_rendered_as_a_grade(self):
|
|
document = copy.deepcopy(self.baseline)
|
|
proposal = self.proposal_batch()
|
|
item = proposal['items'][0]
|
|
item['grade_id'] = None
|
|
item['grading_status'] = 'undetermined'
|
|
item['reason'] = '限定内容存在未解决的正确性问题,暂不可判。'
|
|
document['grade_proposals'] = [proposal]
|
|
process = self.run_catalog(document, 'valid-undetermined-proposal')
|
|
self.assertEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
table = self.run_catalog(document, 'render-undetermined-proposal',
|
|
['--render-catalog', '--proposal-batch', proposal['id']])
|
|
self.assertEqual(table.returncode, 0, table.stdout + table.stderr)
|
|
self.assertIn('| `gongfa-compositional-description` | 组合描述模型 | 解释型 | 暂不可判 |', table.stdout)
|
|
for label in ('missing-status', 'wrong-status'):
|
|
with self.subTest(label=label):
|
|
damaged = copy.deepcopy(document)
|
|
target = damaged['grade_proposals'][0]['items'][0]
|
|
if label == 'missing-status':
|
|
target.pop('grading_status')
|
|
else:
|
|
target['grading_status'] = 'provisional'
|
|
rejected = self.run_catalog(damaged, label)
|
|
self.assertNotEqual(rejected.returncode, 0, rejected.stdout + rejected.stderr)
|
|
|
|
def assessment(self, status='unrated'):
|
|
entry = self.baseline['gongfa'][0]
|
|
policy = self.baseline['policies'][0]
|
|
return {
|
|
'id': 'test-assessment', 'gongfa_id': entry['id'],
|
|
'content_version': entry['content_version'],
|
|
'vocabulary_id': 'gongfa-grade-vocabulary-v1',
|
|
'policy_id': policy['id'], 'policy_version': policy['version'],
|
|
'status': status, 'grade_id': None, 'scope': None, 'evidence_refs': [],
|
|
'reason': '隔离测试:尚无可用判级结果', 'assessor': 'test-fixture',
|
|
'assessed_at': '2026-10-07T00:00:00Z', 'supersedes': None,
|
|
}
|
|
|
|
def test_supported_non_rating_changes(self):
|
|
for label in ('rename', 'new-policy-version', 'unrated', 'undetermined'):
|
|
with self.subTest(label=label):
|
|
document = copy.deepcopy(self.baseline)
|
|
if label == 'rename':
|
|
document['gongfa'][0]['name'] = '同一内容的展示名称'
|
|
elif label == 'new-policy-version':
|
|
policy = copy.deepcopy(document['policies'][0])
|
|
policy['version'] = '0.2.0'
|
|
document['policies'].append(policy)
|
|
else:
|
|
document['assessments'].append(self.assessment(label))
|
|
process = self.run_catalog(document, label)
|
|
self.assertEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
|
|
def test_isolated_rating_record_shape(self):
|
|
# 只验证合成记录的协议形态;这不是本库功法的实际效果证据。
|
|
document = copy.deepcopy(self.baseline)
|
|
references = {}
|
|
for role in ('task-set', 'baseline', 'conditions', 'criteria', 'evidence'):
|
|
relative = '.github/metadata/gongfa/test-' + role + '.json'
|
|
payload = json.dumps({'fixture_role': role}).encode()
|
|
(self.fixture / relative).write_bytes(payload)
|
|
references[role] = {'path': relative, 'sha256': hashlib.sha256(payload).hexdigest()}
|
|
policy = document['policies'][0]
|
|
policy.update(status='defined', unresolved=[], evaluation_contract={
|
|
'task_set_ref': references['task-set'], 'baseline_ref': references['baseline'],
|
|
'conditions_ref': references['conditions'], 'criteria_ref': references['criteria'],
|
|
})
|
|
assessment = self.assessment()
|
|
assessment.update(status='rated', grade_id='huang-initial', evidence_refs=[references['evidence']],
|
|
scope={key: value for key, value in policy['evaluation_contract'].items()
|
|
if key != 'criteria_ref'})
|
|
document['assessments'] = [assessment]
|
|
process = self.run_catalog(document, 'isolated-rated-shape')
|
|
self.assertEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
for label in ('no-evidence', 'no-scope', 'undefined-contract', 'missing-policy-rule',
|
|
'bad-evidence-digest', 'scope-mismatch', 'unresolved-defined-policy',
|
|
'draft-policy'):
|
|
with self.subTest(label=label):
|
|
damaged = copy.deepcopy(document)
|
|
record, damaged_policy = damaged['assessments'][0], damaged['policies'][0]
|
|
if label == 'no-evidence':
|
|
record['evidence_refs'] = []
|
|
elif label == 'no-scope':
|
|
record['scope'] = None
|
|
elif label == 'undefined-contract':
|
|
damaged_policy['evaluation_contract'] = None
|
|
elif label == 'missing-policy-rule':
|
|
damaged_policy['rules'].pop()
|
|
elif label == 'bad-evidence-digest':
|
|
record['evidence_refs'][0]['sha256'] = '0' * 64
|
|
elif label == 'scope-mismatch':
|
|
record['scope']['baseline_ref'] = references['conditions']
|
|
elif label == 'unresolved-defined-policy':
|
|
damaged_policy['unresolved'] = ['还没固定阈值']
|
|
elif label == 'draft-policy':
|
|
damaged_policy['status'] = 'draft'
|
|
process = self.run_catalog(damaged, label)
|
|
self.assertNotEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
|
|
def test_reject_invalid_catalogs(self):
|
|
labels = [
|
|
'tier-order', 'level-order', 'missing-grade', 'extra-tier', 'wrong-tier-level',
|
|
'duplicate-content-version', 'unknown-source', 'bad-anchor', 'bad-text',
|
|
'bad-text-digest', 'bad-snapshot-digest', 'missing-entry', 'default-grade',
|
|
'path-traversal', 'unknown-policy', 'unknown-content-version',
|
|
'unrated-with-grade', 'draft-rated', 'unknown-field', 'self-supersedes',
|
|
'wrong-superseded-target', 'duplicate-policy-version', 'bad-captured-at',
|
|
'predecessor-self', 'predecessor-missing', 'correction-cycle', 'correction-fork',
|
|
'grade-as-type', 'non-content-type',
|
|
]
|
|
for label in labels:
|
|
with self.subTest(label=label):
|
|
document = copy.deepcopy(self.baseline)
|
|
tiers, entry = document['vocabulary']['tiers'], document['gongfa'][0]
|
|
if label == 'tier-order':
|
|
tiers[0], tiers[1] = tiers[1], tiers[0]
|
|
elif label == 'level-order':
|
|
tiers[0]['levels'][0], tiers[0]['levels'][1] = tiers[0]['levels'][1], tiers[0]['levels'][0]
|
|
elif label == 'missing-grade':
|
|
tiers[0]['levels'].pop()
|
|
elif label == 'extra-tier':
|
|
tiers.append(copy.deepcopy(tiers[-1]))
|
|
elif label == 'wrong-tier-level':
|
|
tiers[0]['levels'][0]['id'] = 'tian-initial'
|
|
elif label == 'duplicate-content-version':
|
|
document['gongfa'].append(copy.deepcopy(entry))
|
|
elif label == 'unknown-source':
|
|
entry['source_ref']['source_id'] = 'missing-source'
|
|
elif label == 'bad-anchor':
|
|
entry['source_ref']['heading_id'] = 'missing-anchor'
|
|
elif label == 'bad-text':
|
|
entry['content']['original_text'] += '篡改原文'
|
|
entry['content']['text_sha256'] = hashlib.sha256(entry['content']['original_text'].encode()).hexdigest()
|
|
elif label == 'bad-text-digest':
|
|
entry['content']['text_sha256'] = '0' * 64
|
|
elif label == 'bad-snapshot-digest':
|
|
document['sources'][0]['snapshot_sha256'] = '0' * 64
|
|
elif label == 'missing-entry':
|
|
document['gongfa'].pop()
|
|
elif label == 'default-grade':
|
|
entry['grade_id'] = 'huang-initial'
|
|
elif label == 'path-traversal':
|
|
document['sources'][0]['snapshot_path'] = '../outside.txt'
|
|
elif label == 'unknown-field':
|
|
entry['permanent_power'] = 100
|
|
elif label in ('grade-as-type', 'non-content-type'):
|
|
entry['ontology_type'] = 'huang-initial' if label == 'grade-as-type' else 'software-artifact'
|
|
elif label == 'duplicate-policy-version':
|
|
document['policies'].append(copy.deepcopy(document['policies'][0]))
|
|
elif label == 'bad-captured-at':
|
|
document['sources'][0]['captured_at'] = '2026-10-07T00:00:00'
|
|
elif label.startswith('predecessor-'):
|
|
entry['predecessor_version'] = 'r1' if label == 'predecessor-self' else 'r999'
|
|
elif label.startswith('correction-'):
|
|
first, second = self.assessment(), self.assessment()
|
|
second['id'] = 'second-assessment'
|
|
second['supersedes'] = first['id']
|
|
document['assessments'] = [first, second]
|
|
if label == 'correction-cycle':
|
|
first['supersedes'] = second['id']
|
|
else:
|
|
third = self.assessment()
|
|
third['id'], third['supersedes'] = 'third-assessment', first['id']
|
|
document['assessments'].append(third)
|
|
else:
|
|
assessment = self.assessment()
|
|
document['assessments'].append(assessment)
|
|
if label == 'unknown-policy':
|
|
assessment['policy_id'] = 'missing-policy'
|
|
elif label == 'unknown-content-version':
|
|
assessment['content_version'] = 'r999'
|
|
elif label == 'unrated-with-grade':
|
|
assessment['grade_id'] = 'huang-initial'
|
|
elif label == 'draft-rated':
|
|
assessment.update(status='rated', grade_id='tian-advanced')
|
|
elif label == 'self-supersedes':
|
|
assessment['supersedes'] = assessment['id']
|
|
elif label == 'wrong-superseded-target':
|
|
other = self.assessment()
|
|
other['id'] = 'other-assessment'
|
|
other['gongfa_id'] = document['gongfa'][1]['id']
|
|
document['assessments'].append(other)
|
|
assessment['supersedes'] = other['id']
|
|
process = self.run_catalog(document, label)
|
|
self.assertNotEqual(process.returncode, 0, process.stdout + process.stderr)
|
|
|
|
def test_reject_non_json_and_changed_snapshot(self):
|
|
for label, text in [('duplicate-key', '{"schema_version":"1.0.0","schema_version":"1.0.0"}'),
|
|
('nan', '{"value":NaN}')]:
|
|
with self.subTest(label=label):
|
|
path = ARTIFACTS / (label + '.json')
|
|
path.write_text(text)
|
|
self.assertNotEqual(self.run_path(path, ARTIFACTS / label).returncode, 0)
|
|
source = self.baseline['sources'][0]
|
|
snapshot = self.fixture / source['snapshot_path']
|
|
original = snapshot.read_bytes()
|
|
try:
|
|
snapshot.write_bytes(original + b' changed')
|
|
self.assertNotEqual(self.run_catalog(self.baseline, 'changed-snapshot').returncode, 0)
|
|
finally:
|
|
snapshot.write_bytes(original)
|
|
readme = self.fixture / '.github/metadata/gongfa/README.md'
|
|
original_view = readme.read_text()
|
|
try:
|
|
readme.write_text(original_view.replace('| 黄阶初级 |', '| 黄阶最低档 |'))
|
|
self.assertNotEqual(self.run_catalog(self.baseline, 'changed-grade-view').returncode, 0)
|
|
repair = self.run_catalog(self.baseline, 'render-view-repair', ['--render-grades'])
|
|
self.assertEqual(repair.returncode, 0, repair.stdout + repair.stderr)
|
|
self.assertIn('| `huang-initial` | 黄阶初级 | 黄阶 | 初级 | 1 |', repair.stdout)
|
|
self.assertIn('| `tian-advanced` | 天阶高级 | 天阶 | 高级 | 12 |', repair.stdout)
|
|
finally:
|
|
readme.write_text(original_view)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--artifacts', type=Path)
|
|
args = parser.parse_args()
|
|
ARTIFACTS = args.artifacts or Path(tempfile.mkdtemp(prefix='gongfa-test-'))
|
|
if args.artifacts:
|
|
ARTIFACTS.mkdir(parents=True, exist_ok=False)
|
|
suite = unittest.defaultTestLoader.loadTestsFromTestCase(GongfaProtocolTest)
|
|
test_names = [test.id() for test in suite]
|
|
result = unittest.TextTestRunner(verbosity=2).run(suite)
|
|
summary = {'tests': result.testsRun, 'failures': len(result.failures), 'errors': len(result.errors),
|
|
'success': result.wasSuccessful(), 'artifacts': str(ARTIFACTS)}
|
|
(ARTIFACTS / 'result.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2) + '\n')
|
|
xml = ET.Element('testsuite', name='gongfa-cli', tests=str(result.testsRun),
|
|
failures=str(len(result.failures)), errors=str(len(result.errors)))
|
|
for name in test_names:
|
|
node = ET.SubElement(xml, 'testcase', name=name)
|
|
for test, traceback in result.failures + result.errors:
|
|
if test.id().startswith(name):
|
|
ET.SubElement(node, 'failure', message=str(test)).text = traceback
|
|
ET.ElementTree(xml).write(ARTIFACTS / 'junit.xml', encoding='utf-8', xml_declaration=True)
|
|
print(json.dumps(summary, ensure_ascii=False))
|
|
sys.exit(0 if result.wasSuccessful() else 1)
|