补齐 CI 缓存维护器请求与任务契约校验
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / AI game creator shell Rust smoke (push) Has been cancelled
Project CI / AI game creator shell Rust crates (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled

要求 API 请求显式指定 HTTP 方法并更新全部调用点
增加 workflow 全部任务名称与缓存导出映射的契约测试
从 Rust 任务映射推导集合和数量,消除重复硬编码
同步开发流程中的维护约定
This commit was merged in pull request #467.
This commit is contained in:
2026-09-22 09:10:47 +00:00
parent 705bb1e6b3
commit ff7c5f3589
3 changed files with 53 additions and 15 deletions
+41 -5
View File
@@ -7,6 +7,7 @@ import importlib.util
import io
import json
from pathlib import Path
import re
import tempfile
import unittest
from unittest.mock import patch
@@ -32,7 +33,7 @@ class FakeApi:
def __init__(self):
self.requests: list[str] = []
def request(self, path, body=None, raw=False):
def request(self, path, *, method, body=None, raw=False):
self.requests.append(path)
raise AssertionError(f"unexpected API request: {path}")
@@ -58,6 +59,41 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
def tearDown(self):
self.temporary_directory.cleanup()
def test_api_requires_explicit_method_and_preserves_it_with_body(self):
api = maintenance_module.Api(self.config["api_url"], self.token)
with self.assertRaises(TypeError):
api.request("example", body={"value": 1})
with self.assertRaises(TypeError):
api.request("example")
with patch.object(maintenance_module.urllib.request, "build_opener") as build_opener:
for method in ("GET", "POST", "PUT", "PATCH", "DELETE"):
with self.subTest(method=method):
build_opener.return_value.open.return_value = io.BytesIO(b'{}')
body = {"value": 1} if method in {"POST", "PUT", "PATCH"} else None
self.assertEqual(api.request("example", method=method, body=body), {})
request = build_opener.return_value.open.call_args.args[0]
self.assertEqual(request.get_method(), method)
self.assertEqual(request.data, None if body is None else json.dumps(body).encode())
def test_workflow_jobs_and_cache_producers_match_maintenance_contract(self):
workflow = (SCRIPT.parent.parent / ".gitea/workflows/project-ci.yml").read_text(encoding="utf-8")
# 沿用 workflow 的显式 job/step 格式,枚举实际 job,避免另一份名单漏掉新增项。
sections = re.split(r"(?m)^ ([\w-]+):\s*$", workflow.split("\njobs:\n", 1)[1])
jobs, producers = {}, {}
for job_id, section in zip(sections[1::2], sections[2::2]):
names = re.findall(r"(?m)^ name: (.+)$", section)
self.assertEqual(len(names), 1, f"{job_id}: expected one explicit job name")
name = names[0].strip().strip("\"'")
self.assertNotIn(name, jobs, f"duplicate workflow job name: {name}")
jobs[name] = job_id
if "run: python3 scripts/export-gitea-rust-cache.py" in section:
self.assertIn(f" - name: {maintenance_module.EXPORT_STEP}\n", section)
producers[name] = job_id
self.assertEqual(set(jobs), maintenance_module.JOBS,
"workflow job names drifted from maintainer JOBS")
self.assertEqual(producers, maintenance_module.RUST_JOB_IDS,
"workflow cache producer names/IDs drifted from maintainer RUST_JOB_IDS")
def record(self, image, *, source=SHA, owned=True, verified_run=None, base=BASE_IMAGE):
return {
"image": image,
@@ -255,7 +291,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
class Api:
def pages(self, path, key):
return iter({"workflow_runs": [run], "jobs": jobs, "artifacts": artifacts}[key])
def request(self, path, raw=False):
def request(self, path, *, method, raw=False):
job_id = int(path.split("/jobs/")[1].split("/")[0])
name = next(item["name"] for item in artifacts if item["id"] == job_id)
return f"worker image: {OLD_IMAGE}\n[rust-cache] artifact={name} objects=1 bytes=10 complete=true\n"
@@ -295,7 +331,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
def test_exports_using_different_images_are_not_combined(self):
instance, _, _, _ = self.source_fixture()
original = instance.api.request
instance.api.request = lambda path, raw=False: original(path, raw).replace(OLD_IMAGE, IMAGE) if '/jobs/1/' in path else original(path, raw)
instance.api.request = lambda path, **kwargs: original(path, **kwargs).replace(OLD_IMAGE, IMAGE) if '/jobs/1/' in path else original(path, **kwargs)
self.assertIsNone(self.select_source(instance))
def test_completed_artifact_without_upload_completion_log_is_not_used(self):
@@ -357,7 +393,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
{**artifact, "id": 5, "workflow_run": {"id": 11}}]
deleted = []
instance.api.pages = lambda *_: iter(rows)
def request(path, method=None):
def request(path, *, method):
if method == "DELETE":
return deleted.append((path, method))
return {9: old_run, 10: {**old_run, "event": "pull_request"},
@@ -554,7 +590,7 @@ class GiteaCacheMaintenanceTest(unittest.TestCase):
return iter(jobs)
raise AssertionError(path)
def request(self, path, body=None, raw=False):
def request(self, path, *, method, body=None, raw=False):
if not raw or "/actions/jobs/" not in path:
raise AssertionError(path)
return f"runner image: {OLD_IMAGE}\n"