test(ci): 反向夹具扩到 54 条,含预览发布与合并调用链负例
新增 14~19 节:假 bridge 数 publish 请求(坏报告 0 次/好报告 1 次)、--verify-only 单跑核对、检出协议保留、合并调用链(假 Gitea 跑真实 merge_pr,13 分支)、生成器落齐新脚本与空策略文件。首轮 1 处失败暴露真实缺陷:无远端引用时 --not --remotes 退化成整部历史(把 Initial commit 算进来)⇒ 改为只核 HEAD 并写明。
This commit is contained in:
@@ -146,12 +146,198 @@ PY
|
||||
git rm -q --cached zz_fixture_bad.go; rm -f zz_fixture_bad.go
|
||||
|
||||
say "8. 上报:通知失败不改变质量结果;汇总不可用必须非 0"
|
||||
printf '{"schema":"ci-report/1","overall_status":"success","policy_revision":"ci-checks/1","repository":"fixture/local","commit_sha":"%s","checks":[{"id":"format","status":"success","reason":"ok","duration_s":1}],"duration_s":2}' "$(git rev-parse HEAD)" > /tmp/kg-report-ok.json
|
||||
python3 - "$(git rev-parse HEAD)" <<'PYPY'
|
||||
import json, sys, pathlib
|
||||
sys.path.insert(0, 'scripts')
|
||||
from ci_checks import CHECK_MANIFEST, SUCCESS
|
||||
sha = sys.argv[1]
|
||||
# 夹具报告必须是**合规**的(真清单 + 完整 SHA + run_mode=full):上报脚本现在会独立核对,
|
||||
# 不核对就会把"能跑通"误当成"结论可信"。
|
||||
checks = [{"id": cid, "status": SUCCESS, "reason": "ok", "duration_s": 1, "required_in": list(req)}
|
||||
for cid, req in CHECK_MANIFEST.items()]
|
||||
rep = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full", "commit_sha": sha,
|
||||
"event_sha": sha, "overall_status": SUCCESS, "policy_revision": "fixture",
|
||||
"manifest": {cid: list(req) for cid, req in CHECK_MANIFEST.items()},
|
||||
"checks": checks, "duration_s": 2, "problems": []}
|
||||
pathlib.Path("/tmp/kg-report-ok.json").write_text(json.dumps(rep, ensure_ascii=False), encoding="utf-8")
|
||||
PYPY
|
||||
CI_BRIDGE_RESOLVED="http://127.0.0.1:1" "$PY" .gitea/scripts/ci_report.py --report /tmp/kg-report-ok.json > /tmp/kg-notify.log 2>&1
|
||||
rc=$?
|
||||
if [ "$rc" = 0 ]; then ok "通知不可达时退出码仍是 0(质量结论不变)"; else no "通知失败把退出码带成了 $rc"; fi
|
||||
if [ "$rc" = 0 ]; then ok "通知不可达时退出码仍是 0(质量结论不变)"; else no "通知失败把退出码带成了 $rc"; sed 's/^/ /' /tmp/kg-notify.log | tail -5; fi
|
||||
if grep -q '"notification_status": "failed' /tmp/kg-report-ok.json; then ok "通知失败被单独标注(notification_status=failed)"; else no "没看到 notification_status 被标注"; fi
|
||||
expect 1 "汇总报告缺失 ⇒ 上报脚本非 0(不能报绿)" -- "$PY" .gitea/scripts/ci_report.py --report /tmp/kg-does-not-exist.json
|
||||
|
||||
say "9. 必需/可选测试的区分(必需用例被跳过必须红)"
|
||||
cp scripts/test-scope.json /tmp/kg-scope.bak
|
||||
python3 - <<'PYPY'
|
||||
import json, pathlib
|
||||
p = pathlib.Path('scripts/test-scope.json')
|
||||
d = json.loads(p.read_text(encoding='utf-8'))
|
||||
# 删掉"整个 postgres 包可跳过"这条声明:该包里的 57 个跳过随即变成"必需用例被跳过"
|
||||
d['optional'] = [e for e in d['optional']
|
||||
if e.get('package') != 'knowledge-governance/internal/platform/postgres']
|
||||
p.write_text(json.dumps(d, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
PYPY
|
||||
expect 1 "未声明的必需用例被跳过 ⇒ tests-go 红" -- "$PY" scripts/ci_checks.py --only tests-go
|
||||
cp /tmp/kg-scope.bak scripts/test-scope.json
|
||||
expect 0 "恢复声明后 tests-go 回到绿" -- "$PY" scripts/ci_checks.py --only tests-go
|
||||
mv scripts/test-scope.json /tmp/kg-scope2.bak
|
||||
expect 1 "测试范围声明文件缺失 ⇒ tests-go 红(不许默认放行)" -- "$PY" scripts/ci_checks.py --only tests-go
|
||||
mv /tmp/kg-scope2.bak scripts/test-scope.json
|
||||
|
||||
say "10. 前端 pending/todo 必须红"
|
||||
printf "import { describe, it } from 'vitest'\ndescribe('fixture', () => { it.todo('故意待办') })\n" > web/src/zzPending.test.ts
|
||||
expect 1 "有 todo 待跑用例 ⇒ tests-web 红" -- "$PY" scripts/ci_checks.py --only tests-web
|
||||
rm -f web/src/zzPending.test.ts
|
||||
expect 0 "删除后恢复绿" -- "$PY" scripts/ci_checks.py --only tests-web
|
||||
|
||||
say "11. 上报前必须独立核对报告(不能只看 overall_status)"
|
||||
python3 - "$(git rev-parse HEAD)" <<'PYPY'
|
||||
import json, sys, pathlib
|
||||
sys.path.insert(0, 'scripts')
|
||||
from ci_checks import CHECK_MANIFEST, SUCCESS
|
||||
sha = sys.argv[1]
|
||||
# 合规夹具必须用**真清单**造:报告里的清单与实现不一致时,上报脚本本来就应该拦下来
|
||||
checks = [{"id": cid, "status": SUCCESS, "reason": "ok", "duration_s": 1,
|
||||
"required_in": list(req)} for cid, req in CHECK_MANIFEST.items()]
|
||||
base = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full", "commit_sha": sha,
|
||||
"event_sha": sha, "overall_status": SUCCESS,
|
||||
"manifest": {cid: list(req) for cid, req in CHECK_MANIFEST.items()}, "checks": checks}
|
||||
cases = {
|
||||
"ok": base,
|
||||
"short-sha": {**base, "commit_sha": sha[:12]},
|
||||
"missing-check": {**base, "checks": [c for c in checks if c["id"] != "tests-go"]},
|
||||
"dup-check": {**base, "checks": checks + [checks[0]]},
|
||||
"unknown-check": {**base, "checks": checks + [{"id": "zz", "status": SUCCESS}]},
|
||||
"profile-lie": {**base, "profile": "local"},
|
||||
"manifest-drift": {**base, "manifest": {k: list(v) for k, v in CHECK_MANIFEST.items() if k != "format"}},
|
||||
}
|
||||
for name, rep in cases.items():
|
||||
pathlib.Path(f"/tmp/kg-rep-{name}.json").write_text(json.dumps(rep, ensure_ascii=False), encoding="utf-8")
|
||||
PYPY
|
||||
expect 0 "合规报告 ⇒ 上报脚本 0" -- env GITHUB_ACTIONS=1 GITHUB_SHA="$(git rev-parse HEAD)" "$PY" .gitea/scripts/ci_report.py --no-post --report /tmp/kg-rep-ok.json
|
||||
for c in short-sha missing-check dup-check unknown-check profile-lie manifest-drift; do
|
||||
expect 1 "报告缺陷 $c ⇒ 上报脚本非 0(不能报绿)" -- env GITHUB_ACTIONS=1 GITHUB_SHA="$(git rev-parse HEAD)" "$PY" .gitea/scripts/ci_report.py --no-post --report /tmp/kg-rep-$c.json
|
||||
done
|
||||
|
||||
say "12. 提交闸门用「本次范围」,不用「最近 N 条」"
|
||||
T="$(mktemp -d)"
|
||||
(
|
||||
cd "$T" || exit 1
|
||||
git init -q -b main
|
||||
export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t
|
||||
mkdir -p "$T/no-hooks"
|
||||
export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0="$T/no-hooks"
|
||||
git commit -q --allow-empty -F - <<'M'
|
||||
feat(a): 第一条合规
|
||||
M
|
||||
git commit -q --allow-empty -F - <<'M'
|
||||
update stuff
|
||||
M
|
||||
BAD="$(git rev-parse HEAD)"
|
||||
git commit -q --allow-empty -F - <<'M'
|
||||
fix(b): 第三条合规
|
||||
M
|
||||
"$PY" "$ROOT/.gitea/scripts/ci_gate.py" --range "$BAD..HEAD" > /tmp/kg-range1.log 2>&1
|
||||
echo "clean=$?"
|
||||
"$PY" "$ROOT/.gitea/scripts/ci_gate.py" --range "HEAD~2..HEAD" > /tmp/kg-range2.log 2>&1
|
||||
echo "dirty=$?"
|
||||
) > /tmp/kg-range.log 2>&1
|
||||
grep -q '^clean=0$' /tmp/kg-range.log && ok "干净范围通过(历史里有违规也不误伤 ⇒ 不是固定最近 N 条)" || { no "干净范围竟然判红"; sed 's/^/ /' /tmp/kg-range.log; }
|
||||
grep -q '^dirty=1$' /tmp/kg-range.log && ok "范围含违规提交 ⇒ 判红" || { no "含违规的范围没有判红"; sed 's/^/ /' /tmp/kg-range.log; }
|
||||
rm -rf "$T"
|
||||
|
||||
say "13. 通用模板/生成器:调用同一门禁入口、默认不生成 CD"
|
||||
STD="${ENG_STANDARDS_DIR:-$HOME/eng-standards}"
|
||||
GEN="$(mktemp -d)"
|
||||
if [ -x "$STD/scripts/new-project.sh" ]; then
|
||||
bash "$STD/scripts/new-project.sh" "$GEN/svc" Weknora/svc runner-x > /tmp/kg-gen.log 2>&1
|
||||
if [ -f "$GEN/svc/.gitea/workflows/deploy-production.yml" ]; then no "生成器默认生成了 CD 工作流"; else ok "生成器默认不生成 CD 工作流"; fi
|
||||
[ -f "$GEN/svc/scripts/ci_checks.py" ] && ok "生成器带了权威门禁实现" || no "生成器没带门禁实现"
|
||||
[ -f "$GEN/svc/scripts/verify.sh" ] && ok "生成器带了门禁入口 shim" || no "生成器没带门禁入口"
|
||||
if grep -q 'gofmt -l\|npm run build\|go vet' "$GEN/svc/.gitea/workflows/build.yml"; then
|
||||
no "模板工作流里还留着内联的语言检查(第二套逻辑)"
|
||||
else ok "模板工作流只调用门禁入口(没有内联第二套)"; fi
|
||||
if grep -q '^ deploy' "$GEN/svc/.gitea/workflows/build.yml"; then no "build.yml 里还有 deploy 作业"; else ok "build.yml 里没有 deploy 作业"; fi
|
||||
else
|
||||
no "找不到规范仓的生成器(设 ENG_STANDARDS_DIR 指向它)"
|
||||
fi
|
||||
rm -rf "$GEN"
|
||||
|
||||
say "14. 第二轮修正:预览发布顺序(真实 ci_preview.py + 假 bridge 数请求)"
|
||||
if python3 scripts/ci_preview_negative_test.py >/tmp/kg-preview-neg.log 2>&1; then
|
||||
ok "预览:坏报告一个 publish 请求都不发、合规报告正好发 1 次(10 分支)"
|
||||
else
|
||||
no "预览发布负例失败,详见 /tmp/kg-preview-neg.log"
|
||||
fi
|
||||
|
||||
say "15. 第二轮修正:提交范围取法(11 分支)+ 显示范围异常"
|
||||
if python3 .gitea/scripts/ci_gate.py --self-test >/tmp/kg-scope.log 2>&1; then
|
||||
ok "提交范围:截断/空范围/新分支/手工运行/shallow 全部分支符合预期"
|
||||
else
|
||||
no "提交范围自检失败,详见 /tmp/kg-scope.log"
|
||||
fi
|
||||
|
||||
say "16. 第二轮修正:报告核对可单独调用(--verify-only)"
|
||||
RD="$(mktemp -d)"
|
||||
python3 - "$RD/rep.json" <<'PY'
|
||||
import json, sys
|
||||
sys.path.insert(0, "scripts")
|
||||
import ci_checks
|
||||
good = {"schema": "ci-report/1", "profile": "ci", "run_mode": "full",
|
||||
"commit_sha": "a" * 40, "event_sha": "a" * 40,
|
||||
"manifest": {c: list(r) for c, r in ci_checks.CHECK_MANIFEST.items()},
|
||||
"checks": [{"id": c, "status": "success", "reason": ""} for c in ci_checks.CHECK_MANIFEST],
|
||||
"overall_status": "success"}
|
||||
json.dump(good, open(sys.argv[1], "w"))
|
||||
json.dump({**good, "checks": [c for c in good["checks"] if c["id"] != "tests-go"]}, open(sys.argv[1] + ".missing", "w"))
|
||||
PY
|
||||
GITHUB_ACTIONS=true GITHUB_SHA=$(printf 'a%.0s' {1..40}) python3 .gitea/scripts/ci_report.py --verify-only --report "$RD/rep.json" >/dev/null 2>&1 \
|
||||
&& ok "合规报告:--verify-only 退出 0(可采信)" || no "合规报告竟然核不过"
|
||||
GITHUB_ACTIONS=true GITHUB_SHA=$(printf 'a%.0s' {1..40}) python3 .gitea/scripts/ci_report.py --verify-only --report "$RD/rep.json.missing" >/dev/null 2>&1 \
|
||||
&& no "缺项报告:--verify-only 竟然退出 0" || ok "缺项报告:--verify-only 非 0(不可采信 ⇒ 预览不会发)"
|
||||
rm -rf "$RD"
|
||||
|
||||
say "17. 第二轮修正:检出脚本的协议保留"
|
||||
if [ "$(GITHUB_SERVER_URL='https://gitea.zheromiku.top' GITHUB_REPOSITORY='Weknora/knowledge-governance' bash .gitea/scripts/ci_checkout.sh --print-remote 2>/dev/null)" = "https://gitea.zheromiku.top/Weknora/knowledge-governance.git" ]; then
|
||||
ok "https 服务端 ⇒ remote 仍是 https(没有降级)"
|
||||
else
|
||||
no "https 被降级或拼接不对"
|
||||
fi
|
||||
if GITHUB_SERVER_URL='ftp://nope' GITHUB_REPOSITORY='Weknora/knowledge-governance' bash .gitea/scripts/ci_checkout.sh --print-remote >/dev/null 2>&1; then
|
||||
no "未知协议竟然被接受"
|
||||
else
|
||||
ok "未知协议被拒绝"
|
||||
fi
|
||||
|
||||
say "18. 第二轮修正:合并只认明确策略(真实 merge_pr + 假 Gitea)"
|
||||
if [ -f "$STD/scripts/adopt-existing.py" ]; then
|
||||
if python3 "$STD/scripts/adopt-existing.py" --self-test-chain >/tmp/kg-merge-chain.log 2>&1; then
|
||||
ok "合并调用链:13 分支里只有'策略齐+必需 context 全 success+完整 SHA+base/state 对'才发 merge"
|
||||
else
|
||||
no "合并调用链负例失败,详见 /tmp/kg-merge-chain.log"
|
||||
fi
|
||||
grep -q "required-contexts" "$STD/scripts/adopt-existing.py" \
|
||||
&& ok "接入器只从明确策略取必需 context(不再用观察到的状态)" \
|
||||
|| no "接入器里找不到策略来源"
|
||||
else
|
||||
no "找不到规范仓的接入器"
|
||||
fi
|
||||
|
||||
say "19. 第二轮修正:生成器带齐新脚本与策略文件"
|
||||
if [ -x "$STD/scripts/new-project.sh" ]; then
|
||||
GEN2="$(mktemp -d)"
|
||||
bash "$STD/scripts/new-project.sh" "$GEN2/svc" Weknora/svc2 runner-y >/tmp/kg-gen2.log 2>&1
|
||||
for f in .gitea/scripts/ci_preview.py .gitea/scripts/ci_checkout.sh .gitea/required-contexts.txt scripts/check_secrets.py scripts/test-scope.json; do
|
||||
[ -f "$GEN2/svc/$f" ] && ok "生成器落了 $f" || no "生成器没落 $f"
|
||||
done
|
||||
if grep -qE '^[[:space:]]*build / build' "$GEN2/svc/.gitea/required-contexts.txt"; then
|
||||
no "生成器预填了猜出来的 context(会把没跑过必需检查的 PR 放过去)"
|
||||
else
|
||||
ok "策略文件故意留空(填之前合并会被拒)"
|
||||
fi
|
||||
rm -rf "$GEN2"
|
||||
fi
|
||||
|
||||
printf '\n\033[1m════ 反向验收汇总 ════\033[0m\n ✅ 通过 %d | ❌ 失败 %d\n' "$pass" "$fail"
|
||||
[ "$fail" -eq 0 ] || exit 1
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""预览发布负例:**真实调用路径**上验证"核对不过就一个请求都不发"。
|
||||
|
||||
为什么要有这个文件:只测判定函数(`verify()` 返回几个问题)证明不了"发布顺序"。
|
||||
真正要紧的是:预览这一步到底有没有发出 HTTP 请求。所以这里起一个**假的 ci-bridge**,
|
||||
让它记录收到的每一个请求,然后拿各种坏报告去跑**真实的** `.gitea/scripts/ci_preview.py`,
|
||||
断言坏报告下 publish 请求数 **= 0**、好报告下 **= 1**。
|
||||
|
||||
python3 scripts/ci_preview_negative_test.py
|
||||
|
||||
不联网、不碰真 bridge、不写任何远端状态;只在本机 127.0.0.1 的临时端口上起一个记录用的服务。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import shutil
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
import ci_checks # noqa: E402 用**本仓真实清单**造报告,而不是另编一份
|
||||
|
||||
REQUESTS: list[str] = []
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *a): # 静音(否则每个请求都打一行)
|
||||
pass
|
||||
|
||||
def do_GET(self):
|
||||
REQUESTS.append(f"GET {self.path}")
|
||||
body = b"<html>preview</html>" if self.path.startswith("/preview/") else b"ok"
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
n = int(self.headers.get("Content-Length") or 0)
|
||||
self.rfile.read(n)
|
||||
REQUESTS.append(f"POST {self.path}")
|
||||
payload = json.dumps({"url": f"http://{self.headers.get('Host')}/preview/x/index.html",
|
||||
"url_tailscale": f"http://{self.headers.get('Host')}/preview/x/index.html"})
|
||||
body = payload.encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
|
||||
def good_report(sha: str) -> dict:
|
||||
checks = [{"id": cid, "status": "success", "reason": "", "required_in": list(req), "duration_s": 1}
|
||||
for cid, req in ci_checks.CHECK_MANIFEST.items()]
|
||||
return {"schema": "ci-report/1", "policy_revision": ci_checks.POLICY_REVISION,
|
||||
"repository": "Weknora/knowledge-governance", "commit_sha": sha, "event_sha": sha,
|
||||
"branch": "main", "event": "push", "profile": "ci", "run_mode": "full", "duration_s": 1.0,
|
||||
"checks": checks, "manifest": {c: list(r) for c, r in ci_checks.CHECK_MANIFEST.items()},
|
||||
"overall_status": "success", "problems": [],
|
||||
"notification_status": "not_attempted", "deployed": False}
|
||||
|
||||
|
||||
def run_case(name: str, rep: dict, sha: str, want_posts: int, work: Path) -> tuple[bool, str]:
|
||||
global REQUESTS
|
||||
REQUESTS = []
|
||||
sandbox = work / "repo"
|
||||
shutil.rmtree(sandbox, ignore_errors=True)
|
||||
sandbox.mkdir(parents=True)
|
||||
shutil.copytree(ROOT / ".gitea", sandbox / ".gitea")
|
||||
shutil.copytree(ROOT / "scripts", sandbox / "scripts",
|
||||
ignore=shutil.ignore_patterns("__pycache__"))
|
||||
(sandbox / "web" / "dist").mkdir(parents=True)
|
||||
(sandbox / "web" / "dist" / "index.html").write_text("<html>ok</html>", encoding="utf-8")
|
||||
(sandbox / "ci-report.json").write_text(json.dumps(rep, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
env = {**os.environ, "GITHUB_ACTIONS": "true", "GITHUB_SHA": sha,
|
||||
"GITHUB_REPOSITORY": rep.get("repository", ""), "BRIDGE_TOKEN": "fake-token",
|
||||
"CI_REPORT": str(sandbox / "ci-report.json")}
|
||||
env.pop("GITHUB_RUN_ID", None)
|
||||
p = subprocess.run([sys.executable, str(sandbox / ".gitea" / "scripts" / "ci_preview.py")],
|
||||
cwd=sandbox, env=env, capture_output=True, text=True, timeout=180)
|
||||
posts = [r for r in REQUESTS if r.startswith("POST")]
|
||||
ok = len(posts) == want_posts and p.returncode == 0
|
||||
detail = (f"退出码 {p.returncode}|publish 请求 {len(posts)} 次(期望 {want_posts})"
|
||||
f"|其他请求 {len(REQUESTS) - len(posts)}")
|
||||
if not ok:
|
||||
detail += f"\n stdout: {p.stdout.strip()[-300:]}\n stderr: {p.stderr.strip()[-300:]}"
|
||||
return ok, detail
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sha = "5f3a71cadee1f2b27c0b71d0c9fd1ea1b57a4b0f"
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
port = srv.server_address[1]
|
||||
threading.Thread(target=srv.serve_forever, daemon=True).start()
|
||||
os.environ["CI_BRIDGE_RESOLVED"] = f"http://127.0.0.1:{port}"
|
||||
|
||||
good = good_report(sha)
|
||||
bad = good_report("b" * 40)
|
||||
cases = [
|
||||
# 名字, 报告, 期望的 publish 次数
|
||||
("正例:完整合规报告 ⇒ 发 1 次", good, 1),
|
||||
("缺一项必需检查 ⇒ 0 次", {**good, "checks": [c for c in good["checks"] if c["id"] != "tests-go"]}, 0),
|
||||
("多一项清单外的检查 ⇒ 0 次", {**good, "checks": good["checks"] + [{"id": "zz", "status": "success"}]}, 0),
|
||||
("重复项 ⇒ 0 次", {**good, "checks": good["checks"] + [good["checks"][0]]}, 0),
|
||||
("短 SHA ⇒ 0 次", {**good, "commit_sha": sha[:12], "event_sha": ""}, 0),
|
||||
("归错提交 ⇒ 0 次", bad, 0),
|
||||
("CI 里用 local 档案 ⇒ 0 次", {**good, "profile": "local"}, 0),
|
||||
("只跑子集(not_run_only)⇒ 0 次", {**good, "overall_status": "not_run_only", "run_mode": "only"}, 0),
|
||||
("没有清单 ⇒ 0 次", {k: v for k, v in good.items() if k != "manifest"}, 0),
|
||||
("某项 failure 但自称 success ⇒ 0 次",
|
||||
{**good, "checks": [{**c, "status": "failure"} if c["id"] == "security-deps" else c
|
||||
for c in good["checks"]]}, 0),
|
||||
]
|
||||
problems, lines = [], []
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
for name, rep, want in cases:
|
||||
ok, detail = run_case(name, rep, sha, want, Path(d))
|
||||
lines.append(f"{'✅' if ok else '❌'} {name}:{detail}")
|
||||
if not ok:
|
||||
problems.append(name)
|
||||
srv.shutdown()
|
||||
print("── 预览发布负例(真实 ci_preview.py + 假 bridge 计数)──")
|
||||
for line in lines:
|
||||
print(" " + line)
|
||||
if problems:
|
||||
print(f"\n❌ {len(problems)} 个分支不符合预期:{problems}", file=sys.stderr)
|
||||
return 1
|
||||
print(f"\n✅ {len(cases)} 个分支全部符合预期:坏报告一个 publish 请求都不发,好报告正好发 1 次")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user