Files
vibe-coding-cn/.github/test-source-archive.py
T
tradecatlabsandGitHub 5018bdde1a feat: explanations - 迁移v2说明并保留评级历史 (#253)
迁移443条说明到v2二十项结构,保留来源、书目与历史意见;经验迁移功法暂拟天阶初级。442条概念及443条条目注释仍待核,不主张独立语义审查、运行、效果或正式评级已经完成。
2026-10-25 13:49:28 +08:00

1561 lines
91 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""原始资料JSON存储的行为契约;保存隔离输入与结果,不调用模型或第三方程序。"""
import base64
import hashlib
import importlib.util
import json
from pathlib import Path
import sys
import tempfile
import unittest
ARTIFACTS = Path(tempfile.mkdtemp(prefix='source-archive-test-'))
MODULE = Path(__file__).with_name('source_archive.py')
spec = importlib.util.spec_from_file_location('source_archive_test_subject', MODULE)
archive = importlib.util.module_from_spec(spec)
def document(raw, mode='100644'):
try:
data, encoding = raw.decode('utf-8'), 'utf-8'
except UnicodeDecodeError:
data, encoding = base64.b64encode(raw).decode('ascii'), 'base64'
return {'mode': mode, 'encoding': encoding, 'data': data, 'sha256': hashlib.sha256(raw).hexdigest()}
class SourceArchiveTest(unittest.TestCase):
def setUp(self):
self.root = ARTIFACTS / self._testMethodName
self.path = self.root / '.github/context/gongfa/data.json'
self.path.parent.mkdir(parents=True)
self.data = {'format': 'vibe-coding-cn/source-archive@1', 'kind': 'gongfa',
'source_revision': 'a' * 40, 'documents': {}, 'locations': {}, 'live_sources': {}, 'projections': []}
archive.clear_cache()
def save(self):
self.path.write_text(json.dumps(self.data, ensure_ascii=False), encoding='utf-8')
def test_original_bytes_are_distinct_from_live_implementation(self):
old = '.github/metadata/gongfa/source-original.txt'
self.data['documents'][old] = document('原始\r\n资料\n'.encode('utf-8'))
self.data['locations'][old] = '.github/extensions/current.txt'
self.data['live_sources'][old] = {'path': '.github/extensions/current.txt',
'sha256': hashlib.sha256(b'current implementation').hexdigest()}
live = self.root / '.github/extensions/current.txt'
live.parent.mkdir(parents=True)
live.write_bytes(b'current implementation')
self.save()
self.assertEqual(archive.read_bytes(self.root / old), '原始\r\n资料\n'.encode('utf-8'))
self.assertEqual(archive.physical_path(self.root / old), live)
archive.check_live(self.root)
live.write_bytes(b'unreviewed change')
with self.assertRaises(ValueError): archive.check_live(self.root)
def test_unavailable_roots_skip_only_explicit_live_bindings(self):
inside = '.github/metadata/gongfa/source-inside.txt'
outside = '.github/metadata/gongfa/source-outside.txt'
current_root = self.root / '.github/extensions/uninitialized'
current_root.mkdir(parents=True)
current_file = self.root / '.github/extensions/live.txt'
current_file.write_bytes(b'current implementation')
self.data['documents'] = {
inside: document(b'archived inside'),
outside: document(b'archived outside'),
}
self.data['locations'] = {
inside: '.github/extensions/uninitialized',
outside: '.github/extensions/live.txt',
}
self.data['live_sources'] = {
inside: {'path': '.github/extensions/uninitialized/source.txt',
'sha256': hashlib.sha256(b'unavailable implementation').hexdigest()},
outside: {'path': '.github/extensions/live.txt',
'sha256': hashlib.sha256(b'current implementation').hexdigest()},
}
self.save()
faqi_path = self.root / '.github/context/faqi/data.json'
faqi_path.parent.mkdir(parents=True)
faqi_path.write_text(json.dumps({
'format': 'vibe-coding-cn/source-archive@1', 'kind': 'faqi',
'source_revision': 'b' * 40,
'documents': {'.github/metadata/faqi/source.txt': document(b'archived faqi')},
'locations': {'.github/metadata/faqi/source.txt': '.github/extensions/uninitialized'},
'live_sources': {'.github/metadata/faqi/source.txt': {
'path': '.github/extensions/uninitialized/faqi-source.txt',
'sha256': hashlib.sha256(b'unavailable faqi').hexdigest(),
}},
'projections': [],
}), encoding='utf-8')
with self.assertRaises(FileNotFoundError):
archive.check_live(self.root)
gongfa_only = {'gongfa': ('.github/extensions/uninitialized',)}
with self.assertRaises(FileNotFoundError):
archive.check_live(self.root, unavailable_roots_by_kind=gongfa_only)
unavailable = {
**gongfa_only, 'faqi': ('.github/extensions/uninitialized',),
}
archive.check_live(self.root, unavailable_roots_by_kind=unavailable)
current_file.write_bytes(b'unreviewed change')
with self.assertRaisesRegex(ValueError, '当前实现已变化'):
archive.check_live(self.root, unavailable_roots_by_kind=unavailable)
with self.assertRaisesRegex(ValueError, '来源路径越界'):
archive.check_live(self.root, unavailable_roots_by_kind={
'gongfa': ('../outside',),
})
def test_binary_and_symlink_payloads_roundtrip_without_dereference(self):
self.data['documents'] = {'.github/metadata/gongfa/history.xlsx': document(b'\x00\xff\x01'),
'.github/skills/original-ref': document(b'../fixed-source', '120000')}
self.save()
self.assertEqual(archive.read_bytes(self.root / '.github/metadata/gongfa/history.xlsx'), b'\x00\xff\x01')
self.assertTrue(archive.source_is_symlink(self.root / '.github/skills/original-ref'))
self.assertEqual(archive.source_readlink(self.root / '.github/skills/original-ref'), '../fixed-source')
target = self.root / 'exported'
archive.export_documents(self.root, 'gongfa', target)
self.assertEqual((target / '.github/metadata/gongfa/history.xlsx').read_bytes(), b'\x00\xff\x01')
self.assertTrue((target / '.github/skills/original-ref').is_symlink())
def test_rejects_corruption_unsafe_ids_and_bad_json(self):
for identity in ['../outside', '.github/../outside', '.github/config/auth.json', '.github/.env']:
with self.subTest(identity=identity):
self.data['documents'] = {identity: document(b'not a credential')}
self.save(); archive.clear_cache()
with self.assertRaises(ValueError): archive.read_bytes(self.root / '.github/metadata/gongfa/registry.json')
self.data['documents'] = {'.github/metadata/gongfa/registry.json': document(b'{}')}
self.data['documents']['.github/metadata/gongfa/registry.json']['sha256'] = '0' * 64
self.save(); archive.clear_cache()
with self.assertRaises(ValueError): archive.read_bytes(self.root / '.github/metadata/gongfa/registry.json')
for raw in ['{"format":"x","format":"y"}', '{"format":NaN}']:
self.path.write_text(raw); archive.clear_cache()
with self.assertRaises(ValueError): archive.read_bytes(self.root / '.github/metadata/gongfa/registry.json')
def test_rejects_missing_current_location_target(self):
old = '.github/metadata/gongfa/source-original.txt'
self.data['documents'][old] = document(b'archived source')
self.data['locations'][old] = '.github/extensions/retired-entry.sh'
self.save()
with self.assertRaisesRegex(ValueError, '当前位置映射目标不存在'):
archive.read_bytes(self.root / old)
def test_rejects_live_path_escape_and_changed_input(self):
old = '.github/metadata/gongfa/registry.json'
self.data['documents'][old] = document(b'{}')
self.data['locations'][old] = '../outside'
self.save()
with self.assertRaises(ValueError): archive.read_bytes(self.root / old)
self.data['locations'] = {}; self.save(); archive.clear_cache()
self.assertEqual(archive.read_bytes(self.root / old), b'{}')
self.path.write_text(self.path.read_text() + '\n')
with self.assertRaises(ValueError): archive.check_unchanged(self.root)
archive.clear_cache()
self.path.rename(self.path.with_suffix('.original'))
self.path.symlink_to(self.path.with_suffix('.original'))
with self.assertRaises(ValueError): archive.read_bytes(self.root / old)
def test_book_view_namespaces_headings_links_and_preserves_code(self):
import book_views
text = '# 示例\n\n<a id="目标"></a>\n## 目标\n\n[另一篇](other.md#目标)\n\n[other.md](other.md)\n\n[other[0..4]复核](other.md)\n\n````text\n### 代码不是目录\n```\n````\n'
text += ('\n[校验](../../scripts/check-gongfa.py)\n'
'\n[测试](../../extensions/test-source-archive.py)\n'
'\n[依赖](../../scripts/requirements-gongfa.txt)\n'
'\n[共享库](../../scripts/lib/README.md)\n'
'\n[资料读取](../../scripts/lib/source_archive.py)\n'
'\n[视图模块](../../extensions/lib/book_views.py)\n'
'\n[分类模块](../../lib/taxonomy.py)\n'
'\n[资源边界](../../lib/__init__.py)\n'
'\n[文档目录同步](../../extensions/sync-doc-toc.py)\n'
'\n[法器生成器](../../extensions/sync-faqi-catalog.py)\n'
'\n[功法生成器](../../extensions/sync-gongfa-catalog.py)\n'
'\n[研究抓取](../../extensions/fetch-research-raw.py)\n'
'\n[Claude旧初始化器入口](../../extensions/init_skill.py)\n'
'\n[Claude根级旧初始化器入口](../../init_skill.py)\n'
'\n[Claude官方初始化脚本历史来源](../../tools/external/claude-official-skills/skills/skill-creator/scripts/init_skill.py)\n'
'\n[Skill Seekers历史源码](../../tools/external/Skill_Seekers-development/README.md)\n')
retired_links = {
'../../tools/chat-vault/README.md': 'chat-vault/README.md',
'../../tools/external/my-nvim/README.md': 'my-nvim/README.md',
'../../tools/config/.codex/README.md': 'codex-config/.codex/README.md',
'../../tools/external/MCPlayerTransfer/README.md': 'MCPlayerTransfer/README.md',
'../../tools/external/XHS-image-to-PDF-conversion/README.md': 'XHS-image-to-PDF-conversion/README.md',
'../../tools/external/html-tools-main/xhs%20graphic%20production%20-%201.0.html': 'xhs%20graphic%20production%20-%201.0.html',
'../../tools/external/html-tools-main/xhs%20graphic%20production.html': 'xhs%20graphic%20production.html',
'../../tools/external/html-tools-main/README.md': 'html-tools-main.md',
'../../tools/prompts-library/README.md': 'prompts-library/README.md',
}
for index, link in enumerate(retired_links):
text += f'\n[历史{index}]({link})\n'
retired_cloud_links = {
'../../skills/auto-tmux/SKILL.md': ('blob', '.github/skills/auto-tmux/SKILL.md'),
'../../context/assets/': ('tree', '.github/context/assets'),
'../../context/assets/templates/tool-explanation-template-v1.md': (
'blob', '.github/context/assets/templates/tool-explanation-template-v1.md'),
'../../context/external-resources/README.md': (
'blob', '.github/context/external-resources/README.md'),
}
flattened_context_links = {
'../../context/ai-citation/README.md': 'ai-citation-README.md',
'../../context/metadata/taxonomy.yml': 'taxonomy.yml',
'../../context/prompts/README.md': 'prompts-README.md',
'../../context/getting-started/learning-map.md': 'pi-cli-setup.md',
# 单一代码块不产生章节锚点;历史定位保留在输入,当前入口指向整份提示词。
'../../learning-map.md#milestone-2': 'pi-cli-setup.md',
'../getting-started/learning-map.md#ai-guidance-rules': 'pi-cli-setup.md',
'../getting-started/learning-map.md#milestone-0': 'pi-cli-setup.md',
'../../god.md#milestone-2': 'pi-cli-setup.md',
'../../context/tool-explanation-template-v1.md': 'tool-explanation-template-v1.md',
}
for index, link in enumerate(retired_cloud_links):
text += f'\n[云端退役{index}]({link})\n'
for index, link in enumerate(flattened_context_links):
text += f'\n[平铺{index}]({link})\n'
self.data['documents'] = {
'.github/docs/gongfa/example.md': document(text.encode()),
'.github/docs/gongfa/other.md': document('# 另一篇\n\n## 目标\n'.encode())}
self.save()
rendered = book_views.render_sources(self.root, 'gongfa')
index = self.root / '.github/taxonomy.yml'
index.parent.mkdir(parents=True, exist_ok=True)
index.write_text('documents:\n gongfa:\n - path: .github/context/gongfa/README.md#doc-other\n - path: .github/context/gongfa/README.md#doc-example\n')
self.assertEqual(rendered, book_views.render_sources(self.root, 'gongfa'), '阅读内容与顺序只由本集合JSON决定')
self.assertIn('id="doc-example-目标"', rendered)
self.assertIn('#doc-other-目标', rendered)
self.assertIn('[other.md](#doc-other)', rendered)
self.assertIn('[other[0..4]复核](#doc-other)', rendered)
self.assertIn('[校验](../../check-gongfa.py)', rendered)
self.assertIn('[测试](../../test-source-archive.py)', rendered)
self.assertIn('[依赖](../../requirements-gongfa.txt)', rendered)
self.assertIn('[共享库](../../AGENTS.md#维护脚本与共享库)', rendered)
self.assertIn('[资料读取](../../source_archive.py)', rendered)
self.assertIn('[视图模块](../../book_views.py)', rendered)
self.assertIn('[分类模块](../../taxonomy.py)', rendered)
self.assertIn('[资源边界](../../source_boundaries.py)', rendered)
for label, target in (
('文档目录同步', 'sync-doc-toc.py'),
('法器生成器', 'sync-faqi-catalog.py'),
('功法生成器', 'sync-gongfa-catalog.py'),
('研究抓取', 'fetch-research-raw.py'),
):
self.assertIn(f'[{label}](../../{target})', rendered)
claude_initializer = (
'https://github.com/anthropics/skills/blob/'
'1ed29a03dc852d30fa6ef2ca53a67dc2c2c2c563/'
'skills/skill-creator/scripts/init_skill.py')
self.assertIn(f'[Claude旧初始化器入口]({claude_initializer})', rendered)
self.assertIn(f'[Claude根级旧初始化器入口]({claude_initializer})', rendered)
self.assertIn(f'[Claude官方初始化脚本历史来源]({claude_initializer})', rendered)
self.assertIn(
'[Skill Seekers历史源码](https://github.com/yusufkaraaslan/Skill_Seekers/blob/'
'26638b248279a3b001b651475e0f28975f5ff069/README.md)', rendered)
for index, target in enumerate(retired_links.values()):
revision = ('b0e3454bb865b95bdc70127a431214c384542dee'
if target == 'prompts-library/README.md'
else '44e6fe37a6a871e9327d9f15b23cfef0b7dcbb58')
expected = (f'https://github.com/tradecatlabs/vibe-coding-cn/blob/{revision}'
'/.github/extensions/' + target)
self.assertIn(f'[历史{index}]({expected})', rendered)
for index, (route, path) in enumerate(retired_cloud_links.values()):
expected = (f'https://github.com/tradecatlabs/vibe-coding-cn/{route}/'
f'71bf460cfa894ad45163a9591d2043422974a681/{path}')
self.assertIn(f'[云端退役{index}]({expected})', rendered)
for index, target in enumerate(flattened_context_links.values()):
self.assertIn(f'[平铺{index}](../../{target})', rendered)
self.assertIn('### 代码不是目录', rendered)
self.assertNotIn('id="doc-example-代码不是目录"', rendered)
self.assertEqual(rendered.count('id="doc-example-目标"'), 1)
from markdown_it import MarkdownIt
self.assertIn('<h2>示例</h2>', MarkdownIt('commonmark').render(rendered))
toc = '\n'.join(book_views.source_toc(self.root, 'gongfa'))
self.assertIn('](#doc-example-目标)', toc)
self.assertIn('](#doc-other-目标)', toc)
self.assertNotIn('代码不是目录', toc)
def test_first_project_discloses_cloud_ai_data_flow(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
section = prompt.split('## 先选一个安全、可验证的目标', 1)[1].split('## 功法编排:从问题决定方法', 1)[0]
required = (
'若使用云端或网页版AI',
'提示和交给模型处理的文件内容会发送给服务商处理',
'有权且愿意分享的最少非敏感内容',
'无法确认服务是否本地时,按云端处理或暂停',
)
self.assertTrue(all(item in section for item in required))
def test_first_project_cloud_office_files_check_hidden_content(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
self.assertIn(
'若要把 Office 文档交给云端AI,先在本机用原件副本检查批注、修订记录和文档属性等隐藏信息;无法确认时改为摘录最少非敏感内容或暂停。',
prompt,
)
def test_first_project_cloud_pdf_overlay_is_not_secure_redaction(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
self.assertIn(
'不要把PDF上的黑框或高亮遮挡当作已删除敏感内容;无法确认底层内容已移除时,改为手工摘录最少非敏感文本或暂停。',
prompt,
)
def test_first_project_goal_selection_includes_nonprescriptive_example(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
section = document.split('## 先选一个安全、可验证的目标', 1)[1].split('## 功法编排:从问题决定方法', 1)[0]
required = (
'示例(只演示写法,不是预设或必做任务)',
'模糊目标“整理笔记”',
'验收可观察为“标题与原文一致,顺序符合我给出的目录”',
)
self.assertTrue(all(item in section for item in required))
def test_first_project_prompt_has_optional_pre_ai_method_guess(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
self.assertIn(
'我在看 AI 方案前的暂定判断(可选;问题缺口/候选功法/理由,不确定可写“未知”):【填写】',
prompt,
)
def test_first_project_prompt_handles_missing_workspace_access_safely(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'若你无法直接访问本机工作区',
'说明这一限制,不要声称已读取',
'先停下,由我决定是否提供最少且已脱敏的相关片段',
'不要要求我上传整个项目、凭据或个人敏感资料',
'无法安全提供时将相关信息标为未知,不要猜',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_git_preflight_distinguishes_staged_and_unstaged_changes(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
required = (
'`git status --short`',
'`git diff`(未暂存改动)',
'`git diff --cached`(已暂存改动)',
'只读取与目标相关且确认非敏感的差异',
'未跟踪、归属不明或可能含凭据/个人信息的内容由我在本机核验',
'未确认前不修改',
)
self.assertTrue(all(item in step1 for item in required), step1)
def test_first_project_git_checkpoint_avoids_staging_mixed_file_changes(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
git_step = document.split('## 第 6 步:按需保存 Git 检查点', 1)[1].split('## 完成记录', 1)[0]
required = (
'同一文件同时包含本次和此前改动',
'不要仅凭文件名就整文件暂存',
'每一处都属于本次并获同意',
'无法安全分清时保持不暂存并由我决定',
)
self.assertTrue(all(item in git_step for item in required), git_step)
def test_first_project_git_checkpoint_confirms_repository_root_and_branch(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
git_step = document.split('## 第 6 步:按需保存 Git 检查点', 1)[1].split('## 完成记录', 1)[0]
intro = git_step.strip().split('\n\n', 1)[0]
required = (
'已有仓库建立检查点',
'`git rev-parse --show-toplevel`',
'`git branch --show-current`',
'本次项目路径和预期分支',
'分支输出为空',
'`detached HEAD`',
'无法确认',
'暂停',
'不要自动切换分支',
)
self.assertTrue(all(item in intro for item in required), intro)
def test_first_project_git_commit_reviews_hooks_before_execution(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
git_step = document.split('## 第 6 步:按需保存 Git 检查点', 1)[1].split('## 完成记录', 1)[0]
commit_list = next((block for block in git_step.split('\n\n') if '\n4. ' in block), '')
item4 = commit_list.split('\n4. ', 1)[1] if '\n4. ' in commit_list else ''
required = (
'提交可能运行仓库配置的 Git hooks',
'`pre-commit`',
'`commit-msg`',
'本机核对这些脚本的来源和副作用',
'来源或行为不明时暂停',
'`--no-verify`',
'自动绕过',
)
self.assertTrue(item4 and all(item in item4 for item in required), item4)
def test_first_project_pauses_on_unmerged_git_paths(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
required = (
'`git status --short` 显示存在未合并/冲突路径',
'先暂停并告诉我',
'不要继续修改或暂存',
'不要自动解决冲突或中止合并/变基',
'由我决定下一步',
)
self.assertTrue(all(item in step1 for item in required), step1)
def test_first_project_project_guidance_does_not_expand_authority(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'若工作区含适用的项目指南(如 `AGENTS.md` 或贡献指南)',
'核对相关约束并在方案中体现',
'不能改变你的目标或扩大授权',
'命令和副作用仍按本提示中的确认规则另行确认',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_prompt_traces_key_facts_to_nonsensitive_sources(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'关键事实应附可复核的非敏感来源',
'如用户提供的信息或文件位置/章节',
'无可确认依据时标为推断或未知',
'不称为已核实',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_prompt_does_not_follow_out_of_scope_symlinks(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'不跟随指向工作区外的符号链接',
'不读取其目标',
'若确需外部内容,先说明用途并等我决定是否提供最少脱敏片段',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_step1_prompt_lines_are_bounded(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
self.assertLessEqual(max(map(len, prompt.splitlines())), 220)
def test_first_project_prompt_uses_relative_nonsensitive_source_locators(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'引用本机文件时,仅使用项目内相对位置或章节',
'不提供含个人信息或敏感目录的绝对路径',
'位置本身敏感时用脱敏标签或标“未知”',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_prompt_dates_version_sensitive_facts(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'对可能随版本或时间变化的事实',
'注明适用版本或核对日期',
'无法确认时标“待核”',
'不称为当前有效',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_prompt_excludes_sensitive_workspace_material(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
required = (
'读取前仅选与目标相关且非敏感的资料',
'不得打开或复制凭据文件(如 `.env`)或个人敏感资料',
'若目标相关信息只存在于此类资料,先停下说明缺口',
'由我决定是否提供脱敏摘要',
'无法安全脱敏就标为未知,不要猜或请求原始内容',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_prompt_requires_confirmation_before_external_lookup(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step1 = document.split('## 第 1 步:定义目标与验收', 1)[1].split('## 第 2 步:拆分并实施最小增量', 1)[0]
prompt = step1.split('```text', 1)[1].split('```', 1)[0]
self.assertIn(
'若方案需要联网查询外部资料,先在方案中列明查询目的、目标来源和拟发送的查询内容;我明确确认前不要发出任何外部请求。',
prompt,
)
def test_first_project_step2_sanitizes_output_before_reporting(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step2 = document.split('## 第 2 步:拆分并实施最小增量', 1)[1].split('## 第 3 步:按实际方式运行或观察', 1)[0]
prompt = step2.split('```text', 1)[1].split('```', 1)[0]
self.assertIn(
'完成后先检查准备回报的命令输出;如可能含凭据或个人敏感信息,先脱敏,只提供判断所需的最少内容;无法安全摘录时只报告状态和未验证项。',
prompt,
)
def test_first_project_stops_before_unsafe_retry_after_failure(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step2 = document.split('## 第 2 步:拆分并实施最小增量', 1)[1].split('## 第 3 步:按实际方式运行或观察', 1)[0]
required = (
'若命令失败或中断',
'先核对实际状态和已产生的改动',
'无法确认操作是否已生效或重试是否安全时,暂停说明',
'不自动重跑可能产生副作用的操作',
)
self.assertTrue(all(item in step2 for item in required))
def test_first_project_checks_actual_diff_after_file_changes(self):
document = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
step2 = document.split('## 第 2 步:拆分并实施最小增量', 1)[1].split('## 第 3 步:按实际方式运行或观察', 1)[0]
required = (
'若修改了文件',
'核对可安全读取的实际差异',
'只包含已确认变更',
'无法查看时说明限制并请你核对',
'不凭摘要宣称范围吻合',
)
self.assertTrue(all(item in step2 for item in required))
# 固定学习边界,不把整句或段落布局锁成兼容接口。
def test_first_project_review_offers_optional_retrieval_before_notes(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
required = (
'以下学习练习都可跳过',
'它们不是工作验收',
'也不是使用本指南的条件',
'合上对话和笔记',
'回忆方法选择、证据和适用条件',
'再对照记录补漏',
'不确定处标“未知”',
)
self.assertTrue(all(item in prompt for item in required))
recall_start = prompt.index('合上对话和笔记')
recall = prompt.index('回忆方法选择、证据和适用条件', recall_start)
compare = prompt.index('再对照记录补漏', recall)
self.assertLess(recall_start, recall)
self.assertLess(recall, compare)
def test_first_project_review_offers_optional_spaced_retrieval(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
required = ('隔一段时间再做一次', '间隔由你决定', '不是必做计划')
self.assertTrue(all(item in prompt for item in required))
def test_first_project_offers_optional_code_self_explanation(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
required = (
'若本次改过代码',
'非敏感改动',
'对应一项原定标准',
'手动追踪一个输入',
'对照实际差异与验收证据',
'记下说不清处',
'不要求运行代码',
'不替代测试或验收',
)
self.assertTrue(all(item in prompt for item in required))
def test_god_termux_pkg_upgrade_scope_is_explicit(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
guidance = next((line for line in m2.splitlines() if line.startswith('**Android/Termux:**')), '')
required = (
'Pi 官方 Termux 指南的 `pkg update && pkg upgrade`',
'会更新 Termux 中已安装的软件包',
'范围不限于 Pi 所需的 Node.js/Git',
'请用户核对待更新清单并说明对 Termux 现有工具的影响',
'范围不明或不接受时暂停',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_termux_pkg_upgrade_keeps_interactive_confirmation(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
guidance = next((line for line in m2.splitlines() if line.startswith('**Android/Termux:**')), '')
required = (
'保留交互确认,不要追加自动确认选项',
'APT 路线中 `-y`/`--assume-yes` 会自动回答“是”',
'先让用户看清单并自行决定',
'清单看不懂或确认方式不明时回答“否”并暂停',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_termux_pi_not_found_uses_official_read_only_diagnostics(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m0 = prompt.split('## M0:检查现状', 1)[1].split('## M1:网络环境', 1)[0]
guidance = next((line for line in m0.splitlines() if '`npm prefix -g`' in line), '')
required = (
'Termux若按官方npm路线刚安装后仍找不到`pi`',
'官方故障排查',
'分两轮指导用户在本机检查',
'先只给`npm prefix -g`,等回报后再给`command -v pi`',
'每轮只给一个只读命令并等待回报',
'核对npm全局二进制目录是否在`PATH`',
'不粘贴输出、路径或完整`PATH`',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_m0_install_repair_handoff_co_locates_consent_stop_and_evidence(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'确认未安装或需修复后,先按实际系统说明所选官方安装路线的影响、必要网络目标及恢复方式并等待用户明确同意;拒绝、影响不明或状态仍不清时暂停,不安装。用户同意后,按 M1 只检查该路线需要的网络(代理仍按 M1 条件由用户选择),再按 M2 走当前系统官方安装步骤;完成后按 M3 在同一终端运行 `pi --version`,只有得到版本号才记为 CLI 可用并继续 M4。',
prompt,
)
def test_god_m2_defers_pi_start_until_m40_review(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
self.assertIn(
'安装完成后若出现 `Start pi now? [Y/n]`,明确输入 `n`,不要按 Enter 接受默认启动;先按 M3 核验版本,再到 M4.0 说明目录资源和信任影响后,由用户决定是否启动 Pi。',
m2,
)
def test_god_m2_windows_user_path_prompt_requires_explicit_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split(
'**仅供已选择的备用路线:**', 1
)[0]
required = (
'询问是否将 Pi 目录加入当前用户级 `PATH`',
'持久环境变量变更',
'等待用户明确同意',
'同意后才按 Enter 接受默认值,否则输入 `n`',
'之后按 M3 核验',
'回 M0 按既有错误分支处理',
'不直接改 PATH',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_windows_node_install_prompt_discloses_side_effects_and_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split(
'**仅供已选择的备用路线:**', 1
)[0]
required = (
'Node.js/npm 预检未通过',
'`Install standalone Node.js now? [Y/n]`',
'独立 Node.js 安装到 `%LOCALAPPDATA%\\pi-node`',
'不替换常规 Node',
'将 `current` 目录加入当前用户 `PATH`',
'等待用户明确同意',
'同意才按 Enter 接受默认值',
'否则输入 `n`',
'预检仍失败时停在该步',
'回 M0 再确认后续安装方案',
'不自动换路线',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_windows_portable_git_discloses_recursive_replacement_risk(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split(
'**仅供已选择的备用路线:**', 1
)[0]
required = (
'选择默认 Portable Git 前',
'先在本机检查安装器提示中显示的实际目标目录',
'若该目录已存在但缺少 `bin\\bash.exe`',
'会递归删除整个目录后再替换',
'只有确认目录内容可替换并经用户明确同意时才继续',
'无法确认时输入 `n` 跳过或暂停',
'不要把目录清单发给网页 AI',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_windows_node_install_discloses_replacement_of_managed_directory(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split(
'**仅供已选择的备用路线:**', 1
)[0]
required = (
'接受独立 Node.js 安装前',
'`%LOCALAPPDATA%\\pi-node\\current`',
'先在本机检查',
'下载并校验新包后递归删除整个目录再替换',
'只有确认目录内容可替换并经用户明确同意时才继续',
'否则输入 `n` 或暂停',
'不要把目录清单发给网页 AI',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_macos_homebrew_node_upgrade_requires_explicit_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
posix = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[0]
required = (
'macOS 安装器在 Node.js/npm 预检未通过',
'Homebrew 已安装 Node.js',
'`brew upgrade node`',
'可能影响使用该 Homebrew Node.js 的其他项目',
'先说明并等待用户明确同意',
'输入 `n` 并停止',
'回 M0 重新确认安装路线',
'不自动改用其他包管理器或独立 Node.js',
)
self.assertTrue(posix and all(item in posix for item in required))
def test_god_m2_posix_standalone_node_replacement_requires_local_check(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
posix = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[0]
required = (
'POSIX 安装器在 Node.js/npm 预检未通过',
'`$XDG_DATA_HOME/pi-node`',
'未设置 `XDG_DATA_HOME` 时为 `$HOME/.local/share/pi-node`',
'仅替换下载版本对应的目录',
'若该版本目录已存在,会递归删除后再解压',
'重建 `current` 符号链接',
'接受前先在本机检查此根目录',
'输入 `n` 跳过并回 M0 重新确认',
'不要把目录清单发给网页 AI',
)
self.assertTrue(posix and all(item in posix for item in required))
def test_god_uninstall_managed_root_deletion_requires_local_target_check(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
uninstall = prompt.split('## 可选:用户明确要求卸载时', 1)[1].split('## 技术边界', 1)[0]
required = (
'官方安装器的 **Uninstall Pi**',
'根据当前 `pi` 命令的安装标记定位实际托管安装根目录并递归删除该目录',
'`$PI_CODING_AGENT_DIR/install`',
'`$HOME/.pi/agent/install`',
'自定义安装可能位于其他位置',
'在本机核对实际目录并保留需保存的内容',
'**Do nothing** 并暂停',
'不要把目录清单发给网页 AI',
'该根目录以外的配置、凭据、会话等数据',
)
self.assertTrue(uninstall and all(item in uninstall for item in required))
def test_god_m2_linux_apt_node_upgrade_requires_explicit_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
posix = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[0]
required = (
'Linux 安装器在 Node.js/npm 预检未通过',
'apt 候选版本满足要求',
'`apt-get update`',
'`apt-get install -y nodejs npm`',
'APT 可能升级已安装的指定 Node.js/npm 包',
'这只影响命令指定的 Node.js/npm 包及其依赖',
'影响使用系统 Node.js 的其他项目',
'先说明并等待用户明确同意',
'输入 `n` 并停止',
'回 M0 重新确认安装路线',
'不自动切换独立 Node.js',
)
self.assertTrue(posix and all(item in posix for item in required))
def test_god_m2_windows_portable_git_shellpath_persistence_requires_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split('**仅供已选择的备用路线:**', 1)[0]
required = (
'Portable Git 会把 Bash 路径写入当前用户的 Pi 配置文件',
'`%USERPROFILE%\\.pi\\agent\\settings.json`',
'`shellPath`',
'后续 Pi 会话',
'内置 Bash 工具和 `!` 命令',
'这是持久配置变更',
'先解释并等待用户明确同意',
'同意才按 Enter 接受默认',
'不同意或不确定时输入 `n` 跳过',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_windows_winget_agreements_require_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
windows = m2.split('**Windows 原生 PowerShell 官方安装器:**', 1)[1].split('**仅供已选择的备用路线:**', 1)[0]
required = (
'`--accept-source-agreements`',
'`--accept-package-agreements`',
'WinGet 源协议',
'包安装器出示的许可协议',
'会自动接受',
'抑制交互提示',
'先说明并等待用户明确同意',
'用户已查看并同意',
'不同意或尚未查看时输入 `n` 跳过',
)
self.assertTrue(windows and all(item in windows for item in required))
def test_god_m2_linux_standalone_node_xz_dependency_requires_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
posix = m2.split('**macOS/Linux 官方安装器:**', 1)[1].split('**Windows 原生 PowerShell 官方安装器:**', 1)[0]
standalone = posix.split('POSIX 安装器在 Node.js/npm 预检未通过', 1)[1].split('macOS 安装器在 Node.js/npm 预检未通过', 1)[0]
required = (
'Linux 独立 Node.js 路线若缺少 `xz` 命令',
'`apt-get update`',
'`apt-get install -y xz-utils`',
'`apk add --update-cache xz`',
'sudo',
'系统级解压依赖',
'先说明并等待用户明确同意',
'不同意或影响不明时停止',
'不自动继续或换路线',
)
self.assertTrue(standalone and all(item in standalone for item in required))
def test_god_m2_posix_standalone_node_checksum_is_conditional(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m2 = prompt.split('## M2:安装 Pi CLI', 1)[1].split('## M3:验证 CLI', 1)[0]
posix = m2.split('**macOS/Linux 官方安装器:**', 1)[1].split('**Windows 原生 PowerShell 官方安装器:**', 1)[0]
standalone = posix.split('POSIX 安装器在 Node.js/npm 预检未通过', 1)[1].split('macOS 安装器在 Node.js/npm 预检未通过', 1)[0]
required = (
'`sha256sum`',
'`shasum`',
'只在本机有',
'SHA-256 比对',
'两者都不可用时',
'跳过此校验并继续解压',
'若你把 SHA-256 校验视为继续前提,先暂停',
'不要把 TLS 下载或安装成功说成 SHA 校验已通过',
)
self.assertTrue(standalone and all(item in standalone for item in required))
def test_first_project_optional_transfer_practice_compares_cases_with_evidence_boundary(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
required = (
'以下学习练习都可跳过',
'自选第二情境(只构想即可)',
'比较问题、约束和成功证据',
'哪些可复用、哪些需调整或不适用',
'理由应能回指本次记录',
'未在新情境实际验收前,不说迁移已成功',
)
self.assertTrue(all(item in prompt for item in required))
def test_first_project_offers_conditional_regression_first_guidance(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
self.assertIn(
'| 可稳定复现的软件缺陷 | [开发流程](context/gongfa/README.md#doc-development-process);回归测试先行 | 仅当缺陷可稳定复现且项目已有适用测试环境;先确认修复前测试会失败,再做最小修复;若无测试环境,保留复现步骤与原始现象,标“未验证”,不为此搭建或安装工具 | 修复前失败且修复后通过的同一回归测试;否则为复现证据和未验证记录 |',
prompt,
)
def test_first_project_acceptance_explains_boundary_value_analysis(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
self.assertIn(
'若原定验收标准包含有序范围或阈值,且有可控验证对象,可用边界值分析检查每个边界值及其相邻区间中的最近取值,不必穷举所有输入;记录测试值、预期和实际结果。无可控对象时标“未验证”,不为此搭建测试环境。',
prompt,
)
def test_first_project_acceptance_requires_oracle_or_unverified_stop(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'先依据最初确认的验收标准、规格或你掌握的领域知识确定预期',
'若标准含糊或无法确定预期',
'先停下向你澄清并补齐验收标准',
'标为“未验证”',
'不要用AI或被测实现的输出倒推正确答案',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_offers_limited_relation_check_without_exact_oracle(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若单次预期值难以确定',
'原定标准或可靠领域知识给出了可检验的输入—输出关系',
'比较不同输入是否满足该关系',
'这只提供该关系的部分证据,不代表整体验收通过',
'关系无依据时标“未验证”',
'不要自行假设',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_refreshes_evidence_after_code_changes(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若上次验收后又改了代码',
'旧的通过证据只适用于当时的版本',
'按改动影响选择最相关的检查',
'重新核对新行为及可能受影响的既有标准',
'更新证据记录',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_covers_defined_input_categories(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若验收标准定义了输入的有效/无效类别',
'从每个已定义类别选一个代表值核对预期',
'类别规则不清或缺失时',
'先澄清并补齐验收标准',
'不要自行推断拒绝条件',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_checks_defined_condition_combinations(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'原定标准规定多个条件的组合会改变结果',
'列出条件、可行组合和预期结果',
'按目标选择相关规则核对',
'未检查的规则标“未验证”',
'条件或结果缺失时先澄清',
'不自行补充',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_checks_defined_state_sequences(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若验收标准明确了状态、事件及顺序',
'按标准列出相关事件序列和预期状态/动作',
'核对关键转换',
'未定义的转换先澄清',
'未检查项标“未验证”',
'不自行推断',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_failure_checks_test_conditions_before_fix(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'测试未通过时',
'保留原始现象并定位影响范围',
'核对预期依据、输入、测试/工具与环境',
'区分产品问题和测试条件问题',
'来源不明时先澄清并标“未验证”',
'不为通过而改验收标准',
'确认后只修复必要问题',
'再重跑相关验证',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_prioritizes_checks_under_constraints(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若时间或验证资源不足以检查全部验收标准',
'按你确认的风险/影响安排先后',
'未检查项仍标“未验证”',
'不降低标准',
'不算通过',
'不替你接受剩余风险',
)
self.assertTrue(all(item in acceptance for item in required))
def test_first_project_acceptance_requires_defined_nonfunctional_environment(self):
prompt = Path(__file__).with_name('how-to-complete-work.md').read_text(encoding='utf-8')
acceptance = prompt.split('## 第 4 步:用证据验收', 1)[1].split(
'## 第 5 步:复核并总结迁移方法', 1
)[0]
required = (
'若验收标准规定性能、可用性等属性需要特定测量方法或环境',
'先核对这些条件',
'条件不具备时将此项标为“未验证”',
'不把开发机结果外推到目标环境',
)
self.assertTrue(all(item in acceptance for item in required))
def test_god_share_gist_copies_may_persist(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
required = (
'Gist 可被 clone 或 fork',
'已复制的内容可能独立于原件存在',
'分享前按难以收回处理',
)
self.assertTrue(all(item in m40 for item in required))
def test_god_share_gist_link_is_not_access_control(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
required = (
'GitHub 将这类 private gist 称为 secret gist',
'它并非仅本人可见',
'任何取得 URL 的人都能查看',
'若不能接受链接被他人获取,就不要使用此分享目标。',
)
self.assertTrue(all(item in m40 for item in required))
def test_god_share_upload_and_destination_are_explicit(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
required = (
'Pi 的 `/share` 会上传本会话并生成查看链接',
'有 Radius 认证时工件对 Radius 组织可见',
'否则 Pi 通过 GitHub CLI 创建 private gist',
'`sessionShareDestination` 设置可强制指定任一目的地',
'仅确认内容、目标和可见范围后分享;不确定就停止。',
)
self.assertTrue(all(item in m40 for item in required))
def test_god_no_tools_route_clarifies_manual_bang_command(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
self.assertIn(
'Pi 交互界面的 `!命令` 是你手动发起的终端命令,不是模型工具调用;输出会进入对话。`--no-tools`不禁止你手动输入它,也不使命令变安全;先审阅命令,并避免会暴露敏感信息的输出。',
m40,
)
def test_god_prompt_offers_no_tools_model_only_route_with_limits(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'若只接受模型对话、暂不启用工具,可在本机 `pi --help` 支持时运行 `pi --no-tools --no-extensions`。',
prompt,
)
self.assertIn(
'此模式只用于 M4.1,不验证 M4.2;它不是沙箱或项目内容信任开关。',
prompt,
)
self.assertIn('显式 `-e` 仍会加载', prompt)
self.assertIn(
'若用户尚未在该目录进入 Pi,只有在接受默认工具权限并要验证完整流程时,才运行下方命令。只做 M4.1 模型对话时运行上方无工具命令;需要只读文件分析时运行上方只读分析命令。',
prompt,
)
def test_god_prompt_offers_read_only_file_analysis_mode(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'若要让模型读取非敏感项目文件、但不给模型 Bash、Edit 或 Write 工具,并希望本次不连接Pi内置MCP服务器,也不给模型MCP工具,可在本机 `pi --help` 支持时运行 `pi --tools read --no-mcp --no-extensions --no-skills`。',
prompt,
)
self.assertIn(
'`--tools read`选择`read`;`--no-mcp`停用Pi内置MCP,本次不连接已启用的服务器且不提供MCP工具;它不影响替代内置MCP的显式扩展。',
prompt,
)
self.assertIn(
'`--no-extensions` 禁用已发现、已配置和内置扩展(显式 `-e` 仍会加载)。',
prompt,
)
self.assertIn(
'`--no-skills` 禁用发现和配置的技能;显式 `--skill` 路径仍会加载。',
prompt,
)
self.assertIn(
'Pi 的 `read` 仍按当前账户权限访问路径,不限于当前目录;该选项不是文件系统沙箱,也不验证 M4.2。',
prompt,
)
def test_god_read_only_mode_prevents_builtin_mcp_connections(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
guidance = next((line for line in prompt.splitlines() if '`pi --tools read' in line), '')
required = (
'`pi --tools read --no-mcp --no-extensions --no-skills`',
'`--no-mcp`停用Pi内置MCP',
'本次不连接已启用的服务器',
'不提供MCP工具',
'不影响替代内置MCP的显式扩展',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_bash_only_tool_mode_discloses_remaining_authority(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
guidance = next((line for line in m40.splitlines() if '`pi --tools bash' in line), '')
required = (
'若尚未启动Pi且只想验证M4.2的Bash',
'read/edit/write/MCP工具',
'`pi --tools bash --no-mcp --no-extensions --no-skills`',
'显式`-e`仍会加载',
'显式`--skill`仍会加载',
'不是沙箱或逐命令审批',
'Bash仍按当前账户权限执行shell命令',
'M4.2记为未验证',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_bash_only_mode_prevents_builtin_mcp_connections(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m40 = prompt.split('### M4.0:进入 Pi 交互界面', 1)[1].split('### M4.1:验证对话能力', 1)[0]
guidance = next((line for line in m40.splitlines() if '`pi --tools bash' in line), '')
required = (
'`pi --tools bash --no-mcp --no-extensions --no-skills`',
'`--no-mcp`停用Pi内置MCP',
'本次不连接已启用的服务器',
'不提供MCP工具',
'不影响替代内置MCP的显式扩展',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_read_only_mode_warns_sensitive_files_and_stops_if_present(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'不要让模型读取 `.env`、凭据文件或个人敏感数据;`read` 不会替你检查或脱敏文件内容,读取内容可能进入模型上下文及 Pi 会话。若目录内含不能交给模型的资料,不要在此目录启动该模式;改用不含该资料的目录或停止。',
prompt,
)
def test_god_no_session_does_not_claim_cloud_requests_stay_local(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'使用 OpenAI GPT 时,送入模型上下文的提示和文件内容会发送给 OpenAI 处理。`--no-session` 只影响 Pi 本地会话持久化;它不阻止模型请求,也不控制服务商的数据处理或保留政策。不要发送无权或不愿交给服务商的内容;按当前授权路线和账号核对 OpenAI 官方说明。',
prompt,
)
def test_god_local_model_route_does_not_guarantee_local_data_processing(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'Pi 既支持本机 llama.cpp router,也可配置兼容服务器或代理;“本地模型”名称本身不证明请求只在本机处理。先核实实际服务端点与数据流;未确认前不要发送敏感内容。',
prompt,
)
def test_god_custom_model_command_values_are_reviewed_before_request(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'若自定义 `models.json` 并在凭据或请求头值中使用 `!` 命令,Pi 会在模型请求时执行命令且不会缓存;先审核命令及其权限,不要照抄未知指令。',
prompt,
)
def test_god_prompt_warns_about_mcp_connections_before_pi_start(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'MCP 是一种让 Pi 连接服务器并把服务器的工具和资源提供给模型的协议。启动普通 Pi 会话时,所有已启用的 MCP 服务器会在后台自动连接。项目配置 `.pi/mcp.json` 只在授予项目信任后读取;用户级配置默认位于 `~/.pi/agent/mcp.json`,可跨项目生效。stdio 服务器可启动本机进程,HTTP 服务器可能连接远端服务。首次在已有目录启动普通 Pi 或更改 MCP 配置后,先在本机检查两处是否有未知启用项;来源或目标不明时暂停,不要把配置或凭据发送给网页版 Chat AI。',
prompt,
)
def test_god_m42_requires_the_exact_command_and_stops_on_extra_calls(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertIn(
'只有确认 Pi 本次仅执行上方这条 Bash 只读命令并返回 `local shell OK`,才记为通过。',
prompt,
)
self.assertIn(
'若 Pi 等待确认的命令与上方不符或出现额外工具请求,不要批准;若已自动执行额外操作,停止后续调用,记录为未验证并核对实际影响。',
prompt,
)
self.assertIn(
'用户看到 Pi 仅调用 M4.2 指定的 Bash 只读命令并得到结果。',
prompt,
)
def test_god_openai_connectivity_probe_requires_explicit_user_consent(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if 'api.openai.com/v1/models' in line), '')
required = (
'仅 API Key 路线且确有连通故障时',
'先说明这会向 OpenAI 的 `api.openai.com` 发送 HTTP 请求',
'示例命令不包含 Key/Authorization',
'等待用户明确同意后才提供以下探测',
'拒绝则跳过并记录连通性未验证',
'`curl --connect-timeout 5 --max-time 15 -i https://api.openai.com/v1/models`',
'`curl.exe --connect-timeout 5 --max-time 15 -i https://api.openai.com/v1/models`',
'OAuth 不用此端点',
'HTTP 响应只证明端点响应,不证明认证或模型可用',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_oauth_callback_host_scope_requires_local_review(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if 'PI_OAUTH_CALLBACK_HOST' in line and '`0.0.0.0`' in line), '')
required = (
'`127.0.0.1`',
'全部本地 IPv4 接口',
'防火墙、容器网络和端口转发',
'普通本机登录',
'原因不明,先暂停',
'确认网络可达范围',
'只在本机检查变量',
'不把变量值或回调 URL 发给网页 AI',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_oauth_update_requires_install_channel_and_official_route(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
self.assertRegex(
prompt,
r'若当前版本没有该选项[^\n]*如选择更新[^\n]*确认现有 Pi 的安装渠道[^\n]*按该渠道官方说明操作[^\n]*渠道不明就暂停[^\n]*不猜更新命令',
)
def test_god_openai_model_visibility_does_not_prove_subscription_route(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if '`OPENAI_API_KEY`' in line), '')
required = (
'路线/凭据来源不明时先暂停,不发测试问题',
'Pi 也可从进程环境读取 `OPENAI_API_KEY`',
'`/model` 显示 GPT 不证明使用 ChatGPT 订阅 OAuth',
'由用户在本机确认/选择路线',
'不读取或展示 Key 值',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_openai_custom_base_url_requires_local_endpoint_review(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next(
(paragraph for paragraph in m41.split('\n\n') if 'models.json' in paragraph and '`baseUrl`' in paragraph),
'',
)
required = (
'内置 `openai` provider 设置 `baseUrl` 并将请求路由到代理',
'模型与认证设置仍保持不变',
'provider/模型名不能证明请求直达 OpenAI',
'本机核对实际地址和运营方',
'不把含凭据或私有代理的地址贴给网页 AI',
'无法确认时暂停,不发送测试内容',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_custom_endpoint_discloses_data_recipient_before_use(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next(
(paragraph for paragraph in m41.split('\n\n') if '内置 `openai` provider 设置 `baseUrl`' in paragraph),
'',
)
required = (
'请求中的对话内容',
'配置的认证信息',
'端点',
'运营方',
'数据处理方式可接受',
'无法确认时暂停',
'不发送测试内容',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_openai_remote_http_base_url_requires_transport_check(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next(
(paragraph for paragraph in m41.split('\n\n') if '远端' in paragraph and '`baseUrl`' in paragraph and '`http://' in paragraph),
'',
)
required = (
'HTTP的机密性依赖底层传输',
'HTTPS',
'经确认的加密隧道',
'提示词和授权信息',
'未确认时暂停,不发送',
'本机回环端点',
'容器或远程环境',
'`localhost`的实际指向',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_remote_custom_endpoint_preserves_tls_certificate_validation(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((paragraph for paragraph in m41.split('\n\n') if 'NODE_TLS_REJECT_UNAUTHORIZED=0' in paragraph), '')
required = (
'证书校验失败',
'`NODE_TLS_REJECT_UNAUTHORIZED=0`',
'关闭验证',
'TLS/HTTPS 不安全',
'核对服务端证书和本机信任配置',
'暂停',
'不发送提示词或授权信息',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_models_json_literal_api_key_must_stay_local(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((paragraph for paragraph in m41.split('\n\n') if '`apiKey`' in paragraph and 'models.json' in paragraph), '')
required = (
'字面值',
'不要把真实 API Key 写成文件中的字面值',
'不要共享、提交或截图含Key的 `models.json`',
'已有受信来源在本机运行时注入',
'来源不明就暂停',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_openai_oauth_hostname_is_checked(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if line.startswith('- **订阅 OAuth:**')), '')
required = (
'Pi 1.4.0 当前内置流程使用 OpenAI 官方授权域 `auth.openai.com`',
'输入凭据前核对浏览器地址栏主机名',
'未经官方资料确认的其他域或无法确认来源,不输入凭据',
'取消后按所用 Pi 版本的官方说明复核',
'不要仅凭 URL 含“openai.com”字样判断',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_oauth_browser_account_must_match_user_choice(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if line.startswith('- **订阅 OAuth:**')), '')
required = (
'浏览器若显示已登录的 OpenAI 账号',
'授权前先核对它是否与你选定的账号一致',
'账号不符或无法确认时,取消授权并暂停',
'不替你切换账号',
)
self.assertTrue(guidance and all(item in guidance for item in required), guidance)
def test_god_oauth_callback_conflict_has_safe_local_stop_path(self):
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
m41 = prompt.split('### M4.1:验证对话能力', 1)[1].split('### M4.2:验证实际工具调用', 1)[0]
guidance = next((line for line in m41.splitlines() if '`1455`' in line and 'OAuth state mismatch' in line), '')
required = (
'订阅 OAuth',
'未完成的 OAuth 登录',
'只取消你能确认的那次登录',
'决定是否重试',
'暂停并报告脱敏错误',
'不要终止未知进程',
)
self.assertTrue(guidance and all(item in guidance for item in required))
def test_god_prompt_renders_markdown_with_only_example_code_blocks(self):
from markdown_it import MarkdownIt
import book_views
prompt = Path(__file__).with_name('pi-cli-setup.md').read_text(encoding='utf-8')
markdown = MarkdownIt('commonmark')
tokens = markdown.parse(prompt)
unrendered_emphasis = [
child.content
for token in tokens if token.type == 'inline'
for child in (token.children or [])
if child.type == 'text' and '**' in child.content
]
self.assertEqual(unrendered_emphasis, [])
self.assertEqual((tokens[0].type, tokens[0].tag), ('heading_open', 'h1'))
self.assertTrue(prompt.startswith('# Pi CLI 安装与配置指南\n'))
self.assertIn('## M2:安装 Pi CLI', prompt)
self.assertIn('## M4:配置模型并验证本机能力', prompt)
self.assertIn('### M4.2:验证实际工具调用', prompt)
for religious_language in (
'道成了肉身', '《约翰福音》', '《哥林多前书》', '《提摩太后书》',
'《帖撒罗尼迦前书》', '预备行各样的善事', '恩赐原有分别', '凡事察验',
'圣经', '经文', '圣灵', '恩赐', '信仰', '圣洁', '教会', '基督',
'与神对话', '神的能力', 'biblegateway.com',
):
self.assertNotIn(religious_language, prompt)
self.assertNotRegex(prompt, r'</?(?:details|summary|a)\b')
self.assertNotRegex(prompt, r'\]\(#[^)]+\)')
self.assertNotIn('本代码块全文', prompt)
self.assertIn("printf 'local shell OK\\n'", prompt)
first_reply = next(
(line[2:] for line in prompt.splitlines() if line.startswith('> 我会先确认')),
'',
)
self.assertEqual(first_reply.count('?'), 1)
self.assertNotIn('command -v pi', first_reply)
self.assertNotIn('Get-Command pi -ErrorAction SilentlyContinue', prompt)
for token in ('`', '://', 'pi --version', 'curl', 'npm', 'irm'):
self.assertNotIn(token, first_reply)
for contract in (
'## 字多不看',
'## 给网页版 Chat AI 的任务',
'系统未知只问系统;终端未就绪先帮用户打开',
'系统和终端已明确且没有检查结果时,安排 M0 的只读检查;已有结果直接复用',
'每轮只给当前平台、当前阶段的一个动作',
'等待回报后再继续;不一次展示全部路线',
'不能访问或操作设备、代授权或声称已执行',
'用户停止则只汇报已证实项并停止',
'发命令前确认实际 shell',
'Windows 用 PowerShell(不默认 WSL)',
'只读检查不需额外请示',
'绝不索取或转发密码、API Key、Token、cookie、私钥',
'个人订阅 URL',
'用户误贴凭据时不引用、不使用',
'Pi 按启动账户权限运行,不是沙箱',
'先说明目标、影响和恢复方式,等用户明确同意',
'实际打开后才称已核验',
'模型生成的命令与代码也应视为不可信;执行前先核实用途、目标和影响;无法核实或解释时暂停,不因 AI 给出的而直接运行',
'Android/Termux 只按[Pi 官方指南]',
'Termux 未安装时说明官方来源和安装影响并等用户决定',
'推荐 GitHub/F-Droid;Google Play 版已弃用',
'来源不明或仍用旧版先暂停',
'包更新、依赖安装、共享存储授权前说明影响并等同意',
'仅需访问 Android 文件时启用共享存储',
'在计划使用的终端复用已有 `pi --version` 结果:版本号记作 M3并跳过 M1/M2',
'若“命令未找到”,仅在刚安装/更新或改 PATH 后重开终端重试一次',
'仍失败就核对是否装过、安装渠道及目标终端',
'状态/渠道不明则暂停,不盲目重装',
'确认未安装或需修复后,先按实际系统说明所选官方安装路线的影响、必要网络目标及恢复方式并等待用户明确同意',
'安装专用 M1/M2 仅在 M0 确认 CLI 不可用且用户同意安装/修复时使用',
'M4.1 出现真实模型网络故障时,可按本节代理条件路线处理',
'只检查所选安装路线实际需要的官方分发/依赖源',
'GPT 网络按 M4.1 的登录路线和真实故障检查',
'浏览器可达也不证明终端或模型服务可达',
'API Key 与订阅 OAuth 使用不同服务,不能互相代测',
'不探测未触发的安装源',
'目标可达就不用代理',
'若单次探测超时,只报告超时并暂停,不据此判定目标不可达,也不自动重试;确认目标不可达后,也只有用户选择才处理代理',
'FlClash 是客户端,不提供代理订阅',
'https://github.com/chen08209/FlClash/releases/latest',
'`code` 含义、服务状态、资质、价格、隐私和安全均未核实',
'个人订阅 URL 是凭据',
'绝不索取、转发或写入文件/截图/日志',
'仅用户选 FlClash 时,目标为启用 TUN、系统代理、全局模式',
'说明 TUN/全局模式的路由范围、系统权限、DNS/局域网/隐私/断网影响和恢复步骤',
'拒绝或不支持就停止该路线,不换未授权方案、不报完成',
'恢复时关闭本次启用的三项并还原原模式',
'`--connect-timeout 5 --max-time 15`;超时不证明不可达、不自动重试',
'只用界面显示的本地端口做临时配置,不猜端口、不改 shell 启动文件',
'Node.js 22.19.0+ 与 npm',
'仅已有 Nix 时可选,不为 Pi 安装 Nix',
'安装脚本会下载并执行代码',
'不得由 CI、脚本、非交互 shell 或管理员/root 执行',
'部分系统包操作会请求 `sudo`',
'绝不索取 sudo 密码',
'irm https://pi.dev/install.ps1 | iex',
'权限错误时停止,不用 `sudo npm`',
'只证明 CLI 可用,不证明模型或工具接通',
'目录不是沙箱,也不限制 Pi 访问当前账户可访问的路径',
'Pi 会话可能包含提示词、工具参数、命令输出、文件内容及对话中暴露的凭据;如需导出或分享,先检查并脱敏,未检查前不要分享',
'如不希望本次对话写入 Pi 会话存储,可用 `pi --no-session` 启动;该选项只使用不持久化的内存会话,不改变工具权限,也不代表凭据或其他系统数据不会另行保存',
'默认启用 `read`、`bash`、`edit`、`write` 工具(配置可能改变)',
'先解释信任提示涉及的资源,不自动批准',
'项目中的指令及其他内容可能影响模型;即使拒绝信任,也应将其视为不可信输入,不让它替你作决定或授权额外操作',
'获批加载的项目扩展是可执行代码,会在 Pi 进程内运行并沿用启动账户的权限',
'验证与授权期间,密码、API Key、Token、cookie、个人代理订阅 URL 与 OAuth 链接/码都只在本机处理;网页 AI 不索取或接收,只收脱敏状态或错误。',
'`/login`、`/model` 和问题只在 Pi 输入框输入',
'已有该目录的会话直接复用',
'仅复用环境、路线、模型未变且无新失败信号的证据',
'超时、限额、模型不可用先按错误排查,不当作凭据失效',
'换账号/路线须用户明确选择',
'常规验证不 `/logout`、删除或覆盖 `auth.json`',
'ChatGPT 订阅 OAuth',
'OpenAI API Key',
'当前 `/login openai` 提供 **Sign in with ChatGPT**',
'provider `openai`',
'旧 `openai-codex` 已标为 legacy',
'Pi 0.99.0+ 提供此路径',
'它使用 ChatGPT 订阅,不是 API Key 路线',
'链接只用于本机浏览器',
'最终授权码/回调 URL 仅在 Pi 明确要求时贴回本机 Pi',
'绝不发给网页 AI',
'本地回答不算 OpenAI GPT 验证',
'仅 API Key 路线且确有连通故障时',
'curl --connect-timeout 5 --max-time 15 -I https://github.com',
'curl.exe --connect-timeout 5 --max-time 15 -I https://github.com',
'curl --connect-timeout 5 --max-time 15 -i https://api.openai.com/v1/models',
'curl.exe --connect-timeout 5 --max-time 15 -i https://api.openai.com/v1/models',
'不要加入 Key/Authorization;OAuth 不用此端点',
'HTTP 响应只证明端点响应,不证明认证或模型可用',
'`auth.json` 可能含 API Key/OAuth Token',
'成功证据只能是 Pi 会话中所选 OpenAI GPT 的实际回答',
'网页版 AI 回复、CLI 版本、浏览器可达或 FlClash 已连接都不算',
'本地模型不计 OpenAI GPT,也不静默切换',
'让用户在 Pi 输入框中发送:',
"printf 'local shell OK\\n'",
'不要运行其他命令、读取文件、创建或修改文件、访问网络',
'仍不可用或用户拒绝时,不报通过',
'只有用户提供以下三项真实本机证据',
'用户停止则只汇报已证实项并停止;只有 `pi --version` 有版本号、Pi 中所选 OpenAI GPT 的实际回答、Pi 调用本机 Bash 的只读命令并得到结果这三项本机证据齐备,才报告流程完成;缺项时只报已证实项与缺项。',
'所选 OpenAI GPT 在 Pi 会话中回答了简单问题',
'用户看到 Pi 仅调用 M4.2 指定的 Bash 只读命令并得到结果',
'只装 CLI→模型未验证',
'仅本地模型有回答→OpenAI GPT 未验证',
'不代表其他工具、文件读写、自动化、隔离或生产就绪',
'创建项目、装依赖、部署和业务开发不在范围内',
):
with self.subTest(prompt_contract=contract):
self.assertIn(contract, prompt)
self.assertNotIn('确认环境后,让用户在计划使用 Pi 的终端单独运行:', prompt)
self.assertNotIn('若尚未安装,说明其官方来源与安装影响', prompt)
self.assertNotIn('先确认用户有权使用 OpenAI API', prompt)
self.assertNotIn('完成 OpenAI GPT 主路线(ChatGPT 订阅 OAuth 或 OpenAI API Key)的授权后,让用户用 `/model` 选择该路线实际可用的 GPT 模型,并提出一个无需修改文件的简单问题。', prompt)
self.assertNotIn('curl -I https://github.com', prompt)
self.assertNotIn('curl.exe -I https://github.com', prompt)
self.assertNotIn('curl -i https://api.openai.com/v1/models', prompt)
self.assertNotIn('curl.exe -i https://api.openai.com/v1/models', prompt)
self.assertLess(prompt.index('## 字多不看'), prompt.index('## 给网页版 Chat AI 的任务'))
self.assertLess(prompt.index('## M1:网络环境'), prompt.index('## M2:安装 Pi CLI'))
self.assertLess(prompt.find('### M4.0:进入 Pi 交互界面'), prompt.find('\npi\n'))
self.assertLess(prompt.index('\npi\n'), prompt.index('### M4.1:验证对话能力'))
self.assertNotIn('模型能回答后,建议用户在空目录或非敏感目录启动 Pi。', prompt)
self.assertEqual(
[(token.info, token.content) for token in tokens if token.type == 'fence'],
[
('text', 'pi --version\n'),
('bash', 'curl -fsSL https://pi.dev/install.sh | sh\n'),
('powershell', 'irm https://pi.dev/install.ps1 | iex\n'),
('text', 'pi\n'),
('text', "请只通过当前可用的本机 Bash 工具运行这一条只读命令:printf 'local shell OK\\n'\n"
'不要运行其他命令、读取文件、创建或修改文件、访问网络。只返回结果;若工具不可用,请直接说明。\n'),
],
)
html = markdown.render(prompt)
for rendered in ('<h1>Pi CLI 安装与配置指南</h1>', '<h2>M0:检查现状</h2>',
'<h2>完整配置与使用的完成标准</h2>', '<h2>技术边界</h2>',
'href="https://pi.dev/docs/latest/termux"'):
with self.subTest(rendered_markdown=rendered):
self.assertIn(rendered, html)
self.assertEqual(html.count('<pre>'), 5)
self.assertTrue({'pi-cli-安装与配置指南', 'm0检查现状', '官方资料'}.issubset(
book_views._links.markdown_anchors(Path(__file__).with_name('pi-cli-setup.md'))))
self.assertEqual(book_views.relocated('.github/god.md', 'milestone-2'),
('.github/pi-cli-setup.md', ''))
self.assertEqual(book_views.relocated('.github/first-project.md', 'example'),
('.github/how-to-complete-work.md', 'example'))
self.assertEqual(book_views.relocated('.github/context/getting-started/first-project.md', 'example'),
('.github/how-to-complete-work.md', 'example'))
def test_getting_started_documents_use_descriptive_filenames(self):
root = Path(__file__).parent
self.assertTrue((root / 'pi-cli-setup.md').is_file())
self.assertTrue((root / 'how-to-complete-work.md').is_file())
self.assertFalse((root / 'god.md').exists())
self.assertFalse((root / 'first-project.md').exists())
def test_v2_explanations_remain_separate_from_frozen_source_documents(self):
explanation = {'template_sha256': 'a' * 64, 'items': []}
self.data['explanations_v2'] = explanation
self.save()
archive.clear_cache()
bundle = archive.bundles(self.root)['gongfa']
self.assertEqual(explanation, bundle['data']['explanations_v2'])
self.assertNotIn('explanations_v2', bundle['bytes'])
def test_historical_v1_explanation_payload_is_rejected(self):
self.data['explanations_v1'] = {'template_sha256': 'a' * 64, 'items': []}
self.save()
archive.clear_cache()
with self.assertRaises(ValueError):
archive.bundles(self.root)
def test_retired_v0_explanation_payload_is_rejected(self):
self.data['explanations_v0'] = {'template_sha256': 'a' * 64, 'items': []}
self.save()
archive.clear_cache()
with self.assertRaises(ValueError):
archive.bundles(self.root)
def test_independent_raw_input_remains_readable(self):
raw_root = self.root / 'standalone-input'
path = raw_root / '.github/metadata/gongfa/registry.json'
path.parent.mkdir(parents=True)
path.write_bytes(b'{"independent":"input"}\n')
self.assertEqual(archive.read_bytes(path), b'{"independent":"input"}\n')
if __name__ == '__main__':
try:
spec.loader.exec_module(archive)
result = unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(SourceArchiveTest))
report = {'success': result.wasSuccessful(), 'tests': result.testsRun, 'failures': len(result.failures), 'errors': len(result.errors)}
except (OSError, ImportError) as error:
report = {'success': False, 'error': type(error).__name__}
report['test_sha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
(ARTIFACTS / 'result.json').write_text(json.dumps(report, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'artifacts': str(ARTIFACTS), **report}, ensure_ascii=False))
raise SystemExit(0 if report['success'] else 1)