1316 lines
69 KiB
Python
1316 lines
69 KiB
Python
#!/usr/bin/env python3
|
||
"""内部狗粮运行保障工具的行为测试。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import hashlib
|
||
import importlib.util
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
import tempfile
|
||
import unittest
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
TOOL = Path(__file__).resolve().parents[1] / "dogfood_ops.py"
|
||
OPS_DIR = TOOL.parent
|
||
|
||
MODULE_SPEC = importlib.util.spec_from_file_location("dogfood_ops", TOOL)
|
||
assert MODULE_SPEC is not None and MODULE_SPEC.loader is not None
|
||
DOGFOOD_OPS = importlib.util.module_from_spec(MODULE_SPEC)
|
||
MODULE_SPEC.loader.exec_module(DOGFOOD_OPS)
|
||
|
||
|
||
class DogfoodOpsTest(unittest.TestCase):
|
||
"""通过命令行入口验证运维人员实际使用的行为。"""
|
||
|
||
FIXTURE_STARTED_AT = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
FIXTURE_RC_COMMIT = "1" * 40
|
||
|
||
@classmethod
|
||
def setUpClass(cls) -> None:
|
||
"""生成测试专用签名根,并复制 CLI 到相同的固定相对信任路径。"""
|
||
cls.trust_temp_dir = tempfile.TemporaryDirectory()
|
||
deploy_dir = Path(cls.trust_temp_dir.name) / "deploy"
|
||
(deploy_dir / "dogfood-ops").mkdir(parents=True)
|
||
(deploy_dir / "trust").mkdir(parents=True)
|
||
cls.test_tool = deploy_dir / "dogfood-ops" / "dogfood_ops.py"
|
||
cls.test_private_key = deploy_dir / "trust" / "r1-private.pem"
|
||
cls.test_public_key = deploy_dir / "trust" / "r1-attestation-ed25519.pub"
|
||
shutil.copy2(TOOL, cls.test_tool)
|
||
subprocess.run(
|
||
["openssl", "genpkey", "-algorithm", "ED25519", "-out", str(cls.test_private_key)],
|
||
check=True, capture_output=True,
|
||
)
|
||
subprocess.run(
|
||
["openssl", "pkey", "-in", str(cls.test_private_key), "-pubout", "-out", str(cls.test_public_key)],
|
||
check=True, capture_output=True,
|
||
)
|
||
cls.original_public_key = getattr(DOGFOOD_OPS, "ACCOUNT_REPORT_PUBLIC_KEY", None)
|
||
DOGFOOD_OPS.ACCOUNT_REPORT_PUBLIC_KEY = cls.test_public_key
|
||
|
||
@classmethod
|
||
def tearDownClass(cls) -> None:
|
||
"""恢复模块常量并删除测试签名材料。"""
|
||
if cls.original_public_key is not None:
|
||
DOGFOOD_OPS.ACCOUNT_REPORT_PUBLIC_KEY = cls.original_public_key
|
||
cls.trust_temp_dir.cleanup()
|
||
|
||
def setUp(self) -> None:
|
||
"""每个用例使用独立临时证据根,避免测试相互污染。"""
|
||
self.temp_dir = tempfile.TemporaryDirectory()
|
||
self.root = Path(self.temp_dir.name) / "evidence"
|
||
|
||
def tearDown(self) -> None:
|
||
"""清理测试创建的本地证据。"""
|
||
self.temp_dir.cleanup()
|
||
|
||
def run_tool(self, *args: str) -> subprocess.CompletedProcess[str]:
|
||
"""调用真实 CLI,并保留标准输出供失败断言。"""
|
||
return subprocess.run(
|
||
["python3", str(self.test_tool), *args],
|
||
check=False,
|
||
text=True,
|
||
capture_output=True,
|
||
)
|
||
|
||
def init_wave(self, wave: str) -> Path:
|
||
"""初始化指定波次并返回证据目录。"""
|
||
# 公共通过夹具固定在已过去的可信时间,避免用未来证据伪造刚初始化波次的 readiness。
|
||
result = DOGFOOD_OPS.initialize_wave(
|
||
self.root, wave, now=self.FIXTURE_STARTED_AT, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
self.assertEqual(result, 0)
|
||
return self.root / wave
|
||
|
||
def write_json(self, path: Path, value: Any) -> None:
|
||
"""写入稳定 JSON fixture。"""
|
||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
|
||
def add_attachment(self, wave_dir: Path, name: str, content: str = "evidence") -> tuple[str, str]:
|
||
"""创建目录内证据附件并返回相对路径与哈希。"""
|
||
attachment = wave_dir / "attachments" / name
|
||
attachment.parent.mkdir(exist_ok=True)
|
||
attachment.write_text(content, encoding="utf-8")
|
||
return attachment.relative_to(wave_dir).as_posix(), hashlib.sha256(attachment.read_bytes()).hexdigest()
|
||
|
||
def sign_report(self, report_path: Path, signature_path: Path) -> None:
|
||
"""使用测试专用私钥生成真实 detached signature。"""
|
||
subprocess.run(
|
||
[
|
||
"openssl", "pkeyutl", "-sign", "-inkey", str(self.test_private_key),
|
||
"-rawin", "-in", str(report_path), "-out", str(signature_path),
|
||
],
|
||
check=True,
|
||
capture_output=True,
|
||
)
|
||
report_path.chmod(0o600)
|
||
signature_path.chmod(0o600)
|
||
|
||
def close_valid_wave0(self) -> tuple[Path, datetime]:
|
||
"""按两个实际 UTC 运行日构造已通过收口门的 Wave0。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
self.assertEqual(
|
||
DOGFOOD_OPS.freeze_evidence(
|
||
wave_dir, "2026-07-20", now=datetime(2026, 7, 20, 23, 0, tzinfo=timezone.utc)
|
||
),
|
||
0,
|
||
)
|
||
closed_at = datetime(2026, 7, 21, 9, 1, tzinfo=timezone.utc)
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-21", now=closed_at), 0)
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=closed_at)
|
||
self.assertEqual(gate.run(), 0, gate.errors)
|
||
return wave_dir, closed_at
|
||
|
||
def set_trace_duration(self, wave_dir: Path, sample: dict[str, Any], duration_ms: float) -> None:
|
||
"""修改测试 trace 的结束时间并同步样本哈希。"""
|
||
raw_path = wave_dir / sample["rawRef"]
|
||
payload = json.loads(raw_path.read_text(encoding="utf-8"))
|
||
payload["traceEvents"][1]["ts"] = payload["traceEvents"][0]["ts"] + duration_ms * 1000
|
||
self.write_json(raw_path, payload)
|
||
sample["rawSha256"] = hashlib.sha256(raw_path.read_bytes()).hexdigest()
|
||
|
||
def set_json_time(self, wave_dir: Path, filename: str, keys: tuple[Any, ...], value: str) -> None:
|
||
"""修改一处 JSON 证据时间,供统一时间窗接线测试复用。"""
|
||
path = wave_dir / filename
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
target: Any = payload
|
||
for key in keys[:-1]:
|
||
target = target[key]
|
||
target[keys[-1]] = value
|
||
self.write_json(path, payload)
|
||
|
||
def test_checked_evidence_time_rejects_both_sides_of_wave_window(self) -> None:
|
||
"""统一时间窗必须同时拒绝波次开始前和可信当前时钟之后的证据。"""
|
||
started_at = self.FIXTURE_STARTED_AT
|
||
now = started_at + timedelta(hours=24)
|
||
wave_dir = self.init_wave("wave0")
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness", now=now)
|
||
gate.started_at = started_at
|
||
|
||
self.assertIsNone(gate.checked_evidence_time(
|
||
DOGFOOD_OPS.format_timestamp(started_at - timedelta(seconds=1)), "fixture.before"
|
||
))
|
||
self.assertIsNone(gate.checked_evidence_time(
|
||
DOGFOOD_OPS.format_timestamp(now + timedelta(seconds=1)), "fixture.future"
|
||
))
|
||
self.assertEqual(
|
||
[code for code, _ in gate.errors],
|
||
["evidence-time.window", "evidence-time.window"],
|
||
)
|
||
|
||
def test_all_operational_evidence_times_use_the_same_wave_window(self) -> None:
|
||
"""值班、告警、反馈、人工验收、性能及其余结构化证据时间必须全部接入统一时间窗。"""
|
||
started_at = self.FIXTURE_STARTED_AT
|
||
now = started_at + timedelta(hours=25)
|
||
before = DOGFOOD_OPS.format_timestamp(started_at - timedelta(seconds=1))
|
||
future = DOGFOOD_OPS.format_timestamp(now + timedelta(seconds=1))
|
||
|
||
def mutate_performance(wave_dir: Path, value: str) -> None:
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
sample = payload["samples"][0]
|
||
sample["collectedAt"] = value
|
||
raw_path = wave_dir / sample["rawRef"]
|
||
raw = json.loads(raw_path.read_text(encoding="utf-8"))
|
||
raw["metadata"]["collectedAt"] = value
|
||
self.write_json(raw_path, raw)
|
||
sample["rawSha256"] = hashlib.sha256(raw_path.read_bytes()).hexdigest()
|
||
self.write_json(path, payload)
|
||
|
||
def mutate_tsv(wave_dir: Path, filename: str, old: str, value: str) -> None:
|
||
path = wave_dir / filename
|
||
path.write_text(path.read_text(encoding="utf-8").replace(old, value), encoding="utf-8")
|
||
|
||
scenarios = [
|
||
("config", before, lambda wave_dir, value: self.set_json_time(wave_dir, "config-identity.json", ("capturedAt",), value)),
|
||
("snapshot", future, lambda wave_dir, value: self.set_json_time(wave_dir, "snapshot.json", ("mysql", "capturedAt"), value)),
|
||
("restore", before, lambda wave_dir, value: self.set_json_time(wave_dir, "snapshot.json", ("isolatedRestore", "completedAt"), value)),
|
||
("storage", future, lambda wave_dir, value: self.set_json_time(wave_dir, "storage-audit.json", ("capturedAt",), value)),
|
||
("review", before, lambda wave_dir, value: self.set_json_time(wave_dir, "attachments/review-binding.json", ("approvedAt",), value)),
|
||
("database", future, lambda wave_dir, value: self.set_json_time(wave_dir, "attachments/database.json", ("capturedAt",), value)),
|
||
("manual-play", before, lambda wave_dir, value: self.set_json_time(wave_dir, "manual-play.json", ("games", 0, "playedAt"), value)),
|
||
("performance", future, mutate_performance),
|
||
("alerts", before, lambda wave_dir, value: self.set_json_time(wave_dir, "alerts.json", ("deliveries", 0, "receivedAt"), value)),
|
||
("oncall-start", before, lambda wave_dir, value: mutate_tsv(wave_dir, "oncall.tsv", DOGFOOD_OPS.format_timestamp(started_at), value)),
|
||
("oncall-end", future, lambda wave_dir, value: mutate_tsv(wave_dir, "oncall.tsv", DOGFOOD_OPS.format_timestamp(started_at + timedelta(hours=24)), value)),
|
||
("feedback", before, lambda wave_dir, value: mutate_tsv(wave_dir, "feedback.tsv", DOGFOOD_OPS.format_timestamp(started_at), value)),
|
||
("manual-acceptance", future, lambda wave_dir, value: mutate_tsv(wave_dir, "manual-acceptance.tsv", DOGFOOD_OPS.format_timestamp(started_at), value)),
|
||
("drills", before, lambda wave_dir, value: self.set_json_time(wave_dir, "drills.json", ("drills", 0, "performedAt"), value)),
|
||
("safety", future, lambda wave_dir, value: self.set_json_time(wave_dir, "safety-status.json", ("assessedAt",), value)),
|
||
]
|
||
|
||
for name, invalid_time, mutate in scenarios:
|
||
with self.subTest(evidence=name):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
mutate(wave_dir, invalid_time)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness", now=now)
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(
|
||
any(code == "evidence-time.window" for code, _ in gate.errors),
|
||
f"{name} 未接入统一时间窗:{gate.errors}",
|
||
)
|
||
|
||
def test_wave1_init_requires_closed_wave0(self) -> None:
|
||
"""没有 Wave0 收口回执时不得初始化 Wave1。"""
|
||
result = self.run_tool(
|
||
"init", "--root", str(self.root), "--wave", "wave1",
|
||
"--rc-commit", self.FIXTURE_RC_COMMIT,
|
||
)
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("WAVE0_CLOSE_REQUIRED", result.stderr)
|
||
|
||
def test_manifest_and_freeze_use_tool_generated_trusted_times(self) -> None:
|
||
"""开始与冻结时间必须由工具生成,并拒绝未来或回填日期。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
result = DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
self.assertEqual(result, 0)
|
||
wave_dir = self.root / "wave0"
|
||
manifest = json.loads((wave_dir / "manifest.json").read_text(encoding="utf-8"))
|
||
self.assertEqual(manifest["startedAt"], "2026-07-20T09:00:00Z")
|
||
self.assertEqual(manifest["rcCommit"], self.FIXTURE_RC_COMMIT)
|
||
|
||
backfill = DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-19", now=started_at)
|
||
future = DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-21", now=started_at)
|
||
current = DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-20", now=started_at)
|
||
|
||
self.assertNotEqual(backfill, 0)
|
||
self.assertNotEqual(future, 0)
|
||
self.assertEqual(current, 0)
|
||
freeze = json.loads((wave_dir / "freezes/2026-07-20.sha256.json").read_text(encoding="utf-8"))
|
||
self.assertEqual(freeze["createdAt"], "2026-07-20T09:00:00Z")
|
||
|
||
def test_close_requires_elapsed_runtime_and_every_actual_utc_day(self) -> None:
|
||
"""收口必须达到真实时长,并覆盖从 startedAt 到 close 的每个 UTC 运行日。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-20", now=started_at.replace(hour=23))
|
||
|
||
too_early = DOGFOOD_OPS.EvidenceGate(
|
||
wave_dir, "close", now=started_at + timedelta(hours=23, minutes=59)
|
||
)
|
||
self.assertNotEqual(too_early.run(), 0)
|
||
self.assertTrue(any(code == "wave.duration" for code, _ in too_early.errors))
|
||
|
||
close_time = started_at + timedelta(hours=24, minutes=1)
|
||
missing_day = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=close_time)
|
||
self.assertNotEqual(missing_day.run(), 0)
|
||
self.assertTrue(any(code == "freeze.actual-days" for code, _ in missing_day.errors))
|
||
|
||
def test_same_minute_cannot_forge_three_daily_freezes(self) -> None:
|
||
"""同一时刻只能冻结当天,不能用三个日期伪造 Wave1 三天运行。"""
|
||
now = datetime(2026, 7, 23, 5, 30, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=now, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
|
||
results = [
|
||
DOGFOOD_OPS.freeze_evidence(wave_dir, day, now=now)
|
||
for day in ("2026-07-21", "2026-07-22", "2026-07-23")
|
||
]
|
||
|
||
self.assertEqual(results, [2, 2, 0])
|
||
|
||
def test_close_rejects_forged_freeze_created_at(self) -> None:
|
||
"""手工把 createdAt 改成未来或与文件日期不一致时不得收口。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
freeze_now = datetime(2026, 7, 20, 23, 0, tzinfo=timezone.utc)
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-20", now=freeze_now), 0)
|
||
freeze_path = wave_dir / "freezes/2026-07-20.sha256.json"
|
||
freeze = json.loads(freeze_path.read_text(encoding="utf-8"))
|
||
freeze["createdAt"] = "2026-07-21T23:00:00Z"
|
||
self.write_json(freeze_path, freeze)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=started_at + timedelta(hours=24))
|
||
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == "freeze.created-at" for code, _ in gate.errors))
|
||
|
||
def test_performance_rejects_self_reported_duration_without_raw_gstack_record(self) -> None:
|
||
"""自填 durationMs 且未绑定原始 /gstack 记录时必须失败。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
for sample in payload["samples"]:
|
||
sample["durationMs"] = 2000
|
||
sample.pop("rawRef")
|
||
sample.pop("rawSha256")
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.raw-record", result.stdout)
|
||
|
||
def test_rc_and_game_artifact_hashes_are_independent_and_game_is_bound_three_ways(self) -> None:
|
||
"""RC 制品可不同于游戏包,游戏哈希必须同时绑定审核、运行包和数据库证据。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
rc = json.loads((wave_dir / "rc.json").read_text(encoding="utf-8"))
|
||
chain = json.loads((wave_dir / "main-chain.json").read_text(encoding="utf-8"))
|
||
self.assertNotEqual(rc["rcArtifactSha256"], chain["identity"]["gameArtifactSha256"])
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||
self.assertNotIn("main-chain.rc-artifact", result.stdout)
|
||
self.assertNotIn("main-chain.game-artifact", result.stdout)
|
||
|
||
def test_accounts_rejects_self_reported_counts_without_database_report(self) -> None:
|
||
"""自填人数和 FREE 数不能替代绑定激活批次的权威数据库报告。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
self.write_json(wave_dir / "accounts.json", {
|
||
"schema": "dogfood-accounts/1",
|
||
"batchId": "forged-batch",
|
||
"creatorCount": 3,
|
||
"playerCount": 5,
|
||
"freeQuotaCount": 12,
|
||
})
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("accounts.json schema 必须为 dogfood-accounts/2", result.stdout)
|
||
|
||
def test_accounts_rejects_report_with_incomplete_canonical_account_set(self) -> None:
|
||
"""权威报告必须明确覆盖 dogfoodc01-10 与 dogfoodp01-30,不能只报波次人数。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
accounts = json.loads((wave_dir / "accounts.json").read_text(encoding="utf-8"))
|
||
report_path = wave_dir / accounts["databaseReportRef"]
|
||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||
report["accounts"].pop()
|
||
self.write_json(report_path, report)
|
||
self.sign_report(report_path, wave_dir / accounts["databaseReportSignatureRef"])
|
||
accounts["databaseReportSha256"] = hashlib.sha256(report_path.read_bytes()).hexdigest()
|
||
self.write_json(wave_dir / "accounts.json", accounts)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("accounts.database-report.accounts", result.stdout)
|
||
|
||
def test_accounts_rejects_activation_or_quota_status_drift(self) -> None:
|
||
"""激活 run-id、CLAIMED 账号额度和 12 条 FREE 预留任一漂移都必须失败关闭。"""
|
||
for mutation, expected_code in (
|
||
("activation", "accounts.activation-run-id"),
|
||
("captured-at", "accounts.database-report.captured-at"),
|
||
("claimed-quota", "accounts.database-report.account-quota"),
|
||
("token-disabled", "accounts.database-report.account-quota"),
|
||
("token-balance", "accounts.database-report.account-quota"),
|
||
("token-unlimited", "accounts.database-report.account-quota"),
|
||
("token-expiry", "accounts.database-report.account-quota"),
|
||
("newapi-user-duplicate", "accounts.database-report.account-quota"),
|
||
("newapi-token-duplicate", "accounts.database-report.account-quota"),
|
||
("reserve-status", "accounts.database-report.reserve-quota"),
|
||
):
|
||
with self.subTest(mutation=mutation):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
accounts_path = wave_dir / "accounts.json"
|
||
accounts = json.loads(accounts_path.read_text(encoding="utf-8"))
|
||
report_path = wave_dir / accounts["databaseReportRef"]
|
||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||
if mutation == "activation":
|
||
report["activationRunId"] = "different-activation"
|
||
elif mutation == "captured-at":
|
||
started_at = datetime.fromisoformat(
|
||
json.loads((wave_dir / "manifest.json").read_text(encoding="utf-8"))["startedAt"]
|
||
.replace("Z", "+00:00")
|
||
)
|
||
stale_time = DOGFOOD_OPS.format_timestamp(started_at - timedelta(hours=3))
|
||
report["capturedAt"] = stale_time
|
||
accounts["databaseReportCapturedAt"] = stale_time
|
||
elif mutation == "claimed-quota":
|
||
report["accounts"][0]["grantQuota"] = 0
|
||
elif mutation == "token-disabled":
|
||
report["accounts"][0]["tokenStatus"] = "DISABLED"
|
||
elif mutation == "token-balance":
|
||
report["accounts"][0]["tokenRemainQuota"] = 0
|
||
elif mutation == "token-unlimited":
|
||
report["accounts"][0]["tokenUnlimitedQuota"] = True
|
||
elif mutation == "token-expiry":
|
||
report["accounts"][0]["tokenExpiredTime"] = 1800000000
|
||
elif mutation == "newapi-user-duplicate":
|
||
report["accounts"][1]["newapiUserId"] = report["accounts"][0]["newapiUserId"]
|
||
elif mutation == "newapi-token-duplicate":
|
||
report["accounts"][1]["newapiTokenId"] = report["accounts"][0]["newapiTokenId"]
|
||
else:
|
||
report["reserves"][0]["quotaStatus"] = "CLAIMED"
|
||
self.write_json(report_path, report)
|
||
self.sign_report(report_path, wave_dir / accounts["databaseReportSignatureRef"])
|
||
accounts["databaseReportSha256"] = hashlib.sha256(report_path.read_bytes()).hexdigest()
|
||
self.write_json(accounts_path, accounts)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == expected_code for code, _ in gate.errors), gate.errors)
|
||
|
||
def test_accounts_rejects_selected_identity_not_present_in_report(self) -> None:
|
||
"""波次成员必须按 ID+用户名精确引用权威报告中的同一账号。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
accounts_path = wave_dir / "accounts.json"
|
||
accounts = json.loads(accounts_path.read_text(encoding="utf-8"))
|
||
accounts["selectedAccounts"][0]["username"] = "dogfoodc10"
|
||
self.write_json(accounts_path, accounts)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == "accounts.selected.identity" for code, _ in gate.errors), gate.errors)
|
||
|
||
def test_accounts_rejects_missing_or_tampered_detached_signature(self) -> None:
|
||
"""缺签或报告签名后被篡改时必须失败关闭。"""
|
||
for mutation in ("missing", "tampered"):
|
||
with self.subTest(mutation=mutation):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
accounts = json.loads((wave_dir / "accounts.json").read_text(encoding="utf-8"))
|
||
signature_path = wave_dir / accounts["databaseReportSignatureRef"]
|
||
if mutation == "missing":
|
||
signature_path.unlink()
|
||
else:
|
||
report_path = wave_dir / accounts["databaseReportRef"]
|
||
report_path.write_text(report_path.read_text(encoding="utf-8") + " ", encoding="utf-8")
|
||
accounts["databaseReportSha256"] = hashlib.sha256(report_path.read_bytes()).hexdigest()
|
||
self.write_json(wave_dir / "accounts.json", accounts)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(
|
||
any(code == "accounts.database-report.signature" for code, _ in gate.errors),
|
||
gate.errors,
|
||
)
|
||
|
||
def test_accounts_rejects_signed_issuer_rc_or_query_drift(self) -> None:
|
||
"""即使签名有效,固定签发者、manifest RC 和固定查询身份也不能漂移。"""
|
||
for field, value, expected_code in (
|
||
("issuer", "operator-self-signed", "accounts.database-report.issuer"),
|
||
("rcCommit", "2" * 40, "accounts.database-report.rc-commit"),
|
||
("queryId", "handwritten-query", "accounts.database-report.query"),
|
||
("querySchema", "handwritten-row/1", "accounts.database-report.query"),
|
||
):
|
||
with self.subTest(field=field):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
accounts_path = wave_dir / "accounts.json"
|
||
accounts = json.loads(accounts_path.read_text(encoding="utf-8"))
|
||
report_path = wave_dir / accounts["databaseReportRef"]
|
||
report = json.loads(report_path.read_text(encoding="utf-8"))
|
||
report[field] = value
|
||
self.write_json(report_path, report)
|
||
self.sign_report(report_path, wave_dir / accounts["databaseReportSignatureRef"])
|
||
accounts["databaseReportSha256"] = hashlib.sha256(report_path.read_bytes()).hexdigest()
|
||
self.write_json(accounts_path, accounts)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == expected_code for code, _ in gate.errors), gate.errors)
|
||
|
||
def populate_valid_evidence(self, wave_dir: Path) -> None:
|
||
"""构造不依赖 staging 的完整本地通过 fixture。"""
|
||
manifest = json.loads((wave_dir / "manifest.json").read_text(encoding="utf-8"))
|
||
wave = manifest["wave"]
|
||
started_at = datetime.fromisoformat(manifest["startedAt"].replace("Z", "+00:00"))
|
||
captured_at = DOGFOOD_OPS.format_timestamp(started_at)
|
||
played_at = DOGFOOD_OPS.format_timestamp(started_at + timedelta(seconds=1))
|
||
feed_entered_at = DOGFOOD_OPS.format_timestamp(started_at + timedelta(seconds=2))
|
||
commit = "1" * 40
|
||
rc_artifact_ref, rc_artifact_hash = self.add_attachment(wave_dir, "rc-artifact.bin", "rc-package")
|
||
runtime_ref, game_artifact_hash = self.add_attachment(wave_dir, "game-runtime.zip", "game-package")
|
||
config_ref, config_hash = self.add_attachment(wave_dir, "config-bundle.txt")
|
||
dump_ref, dump_hash = self.add_attachment(wave_dir, "mysql.sql.gz")
|
||
restore_ref, _ = self.add_attachment(wave_dir, "restore-report.json")
|
||
storage_ref, storage_hash = self.add_attachment(wave_dir, "storage-audit.json")
|
||
common_ref, _ = self.add_attachment(wave_dir, "browser-proof.png")
|
||
network_ref, _ = self.add_attachment(wave_dir, "main-chain.har")
|
||
log_ref, _ = self.add_attachment(wave_dir, "application.log")
|
||
review_ref, _ = self.add_attachment(wave_dir, "review-binding.json")
|
||
database_ref, _ = self.add_attachment(wave_dir, "database.json")
|
||
account_report_ref = "attachments/account-database-report.json"
|
||
|
||
game_identity = {
|
||
"projectId": "p-1",
|
||
"versionId": "v-1",
|
||
"gameId": "g-1",
|
||
"gameArtifactSha256": game_artifact_hash,
|
||
}
|
||
self.write_json(wave_dir / review_ref, {
|
||
"schema": "dogfood-review-artifact-binding/1",
|
||
**game_identity,
|
||
"reviewId": "review-1",
|
||
"status": "APPROVED",
|
||
"approvedAt": captured_at,
|
||
})
|
||
self.write_json(wave_dir / database_ref, {
|
||
"schema": "dogfood-main-chain-db-binding/1",
|
||
**game_identity,
|
||
"queryId": "query-1",
|
||
"capturedAt": captured_at,
|
||
})
|
||
|
||
self.write_json(wave_dir / "rc.json", {
|
||
"schema": "dogfood-rc/2", "commit": commit, "repoClean": True,
|
||
"rcArtifactRef": rc_artifact_ref, "rcArtifactSha256": rc_artifact_hash,
|
||
})
|
||
self.write_json(wave_dir / "config-identity.json", {
|
||
"schema": "dogfood-config-identity/1", "capturedAt": captured_at,
|
||
"rcCommit": commit, "configBundleRef": config_ref, "configSha256": config_hash,
|
||
})
|
||
self.write_json(wave_dir / "snapshot.json", {
|
||
"schema": "dogfood-snapshot/1",
|
||
"mysql": {"snapshotRef": dump_ref, "sha256": dump_hash, "capturedAt": captured_at},
|
||
"isolatedRestore": {"result": "PASS", "target": "isolated-dogfood-restore-001", "completedAt": captured_at, "evidenceRef": restore_ref},
|
||
})
|
||
self.write_json(wave_dir / "storage-audit.json", {
|
||
"schema": "dogfood-storage-audit/1", "result": "PASS", "capturedAt": captured_at,
|
||
"reportRef": storage_ref, "reportSha256": storage_hash,
|
||
})
|
||
all_accounts = []
|
||
for index in range(1, 11):
|
||
all_accounts.append({
|
||
"id": 1000 + index,
|
||
"username": f"dogfoodc{index:02d}",
|
||
"role": "creator",
|
||
"accountStatus": "ACTIVE",
|
||
"quotaPoolId": 2000 + index,
|
||
"quotaStatus": "CLAIMED",
|
||
"grantQuota": 6849315,
|
||
"newapiUserId": 4000 + index,
|
||
"newapiTokenId": 5000 + index,
|
||
"newapiUsername": f"neice_{index:03d}",
|
||
"tokenStatus": "ENABLED",
|
||
"tokenRemainQuota": 6849315,
|
||
"tokenUnlimitedQuota": False,
|
||
"tokenExpiredTime": -1,
|
||
})
|
||
for index in range(1, 31):
|
||
all_accounts.append({
|
||
"id": 1100 + index,
|
||
"username": f"dogfoodp{index:02d}",
|
||
"role": "player",
|
||
"accountStatus": "ACTIVE",
|
||
"quotaPoolId": 2100 + index,
|
||
"quotaStatus": "CLAIMED",
|
||
"grantQuota": 6849315,
|
||
"newapiUserId": 4100 + index,
|
||
"newapiTokenId": 5100 + index,
|
||
"newapiUsername": f"neice_{10 + index:03d}",
|
||
"tokenStatus": "ENABLED",
|
||
"tokenRemainQuota": 6849315,
|
||
"tokenUnlimitedQuota": False,
|
||
"tokenExpiredTime": -1,
|
||
})
|
||
reserves = [{
|
||
"poolId": 3000 + index,
|
||
"newapiUserId": 4200 + index,
|
||
"newapiTokenId": 5200 + index,
|
||
"newapiUsername": f"neice_{40 + index:03d}",
|
||
"quotaStatus": "FREE",
|
||
"grantQuota": 6849315,
|
||
"tokenStatus": "ENABLED",
|
||
"tokenRemainQuota": 6849315,
|
||
"tokenUnlimitedQuota": False,
|
||
"tokenExpiredTime": -1,
|
||
} for index in range(1, 13)]
|
||
activation_run_id = "activation-20260723T010203Z"
|
||
self.write_json(wave_dir / account_report_ref, {
|
||
"schema": "dogfood-account-database-report/2",
|
||
"issuer": "r1-mini-infra",
|
||
"activationRunId": activation_run_id,
|
||
"rcCommit": commit,
|
||
"capturedAt": captured_at,
|
||
"queryId": "dogfood-account-readiness-v2",
|
||
"querySchema": "dogfood-account-readiness-row/2",
|
||
"accounts": all_accounts,
|
||
"reserves": reserves,
|
||
})
|
||
account_signature_ref = account_report_ref + ".sig"
|
||
self.sign_report(wave_dir / account_report_ref, wave_dir / account_signature_ref)
|
||
account_report_hash = hashlib.sha256((wave_dir / account_report_ref).read_bytes()).hexdigest()
|
||
selected = (
|
||
all_accounts[:3] + all_accounts[10:15]
|
||
if wave == "wave0"
|
||
else all_accounts
|
||
)
|
||
self.write_json(wave_dir / "accounts.json", {
|
||
"schema": "dogfood-accounts/2",
|
||
"activationRunId": activation_run_id,
|
||
"databaseReportRef": account_report_ref,
|
||
"databaseReportSignatureRef": account_signature_ref,
|
||
"databaseReportSha256": account_report_hash,
|
||
"databaseReportCapturedAt": captured_at,
|
||
"selectedAccounts": [
|
||
{"id": account["id"], "username": account["username"]}
|
||
for account in selected
|
||
],
|
||
})
|
||
|
||
permission_cases = {
|
||
"creator-own-project": "ALLOW",
|
||
"creator-cross-account-denied": "DENY",
|
||
"reviewer-review-only": "ALLOW",
|
||
"reviewer-config-write-denied": "DENY",
|
||
"readonly-write-denied": "DENY",
|
||
}
|
||
self.write_json(wave_dir / "role-permissions.json", {
|
||
"schema": "dogfood-role-permissions/1",
|
||
"checks": [{"case": case, "result": result, "screenshotRef": common_ref} for case, result in permission_cases.items()],
|
||
})
|
||
|
||
evidence_bundle = {"screenshotRef": common_ref, "networkRef": network_ref, "logRef": log_ref, "databaseRef": database_ref}
|
||
stages = ["create", "generate", "preview", "submit-review", "approve", "feed", "play", "telemetry"]
|
||
self.write_json(wave_dir / "main-chain.json", {
|
||
"schema": "dogfood-main-chain/2",
|
||
"identity": {
|
||
**game_identity,
|
||
"reviewRecordRef": review_ref,
|
||
"runtimePackageRef": runtime_ref,
|
||
"mainChainDatabaseRef": database_ref,
|
||
},
|
||
"stages": [{"stage": stage, "result": "PASS", **evidence_bundle} for stage in stages],
|
||
})
|
||
|
||
negative_cases = [
|
||
"draft-publish", "reviewing-publish", "rejected-publish", "direct-publish-bypass",
|
||
"artifact-drift", "delisted-feed", "delisted-detail", "delisted-deep-link",
|
||
]
|
||
self.write_json(wave_dir / "release-negatives.json", {
|
||
"schema": "dogfood-release-negatives/1",
|
||
"checks": [{"case": case, "blocked": True, "evidenceRef": common_ref} for case in negative_cases],
|
||
})
|
||
self.write_json(wave_dir / "manual-play.json", {
|
||
"schema": "dogfood-manual-play/1", "releasedGameCount": 1,
|
||
"games": [{"gameId": "g-1", "tester": "tester-1", "playedAt": played_at, "feedEnteredAt": feed_entered_at, "screenshotRef": common_ref, "source": "/gstack@mini-desktop"}],
|
||
})
|
||
|
||
samples = []
|
||
for index in range(30):
|
||
navigation_id = f"nav-{index:02d}"
|
||
collector = {"tool": "/gstack", "host": "mini-desktop", "sessionId": f"session-{index:02d}"}
|
||
raw_ref, raw_hash = self.add_attachment(wave_dir, f"perf-{index:02d}.trace.json")
|
||
self.write_json(wave_dir / raw_ref, {
|
||
"metadata": {"collector": collector, "collectedAt": captured_at},
|
||
"traceEvents": [
|
||
{"name": "dogfood.navigationCommit", "ts": 1_000_000, "args": {"navigationId": navigation_id}},
|
||
{"name": "dogfood.firstInteractiveGameFrame", "ts": 3_000_000, "args": {"navigationId": navigation_id}},
|
||
],
|
||
})
|
||
raw_hash = hashlib.sha256((wave_dir / raw_ref).read_bytes()).hexdigest()
|
||
samples.append({
|
||
"navigationId": navigation_id,
|
||
"cache": "cold",
|
||
"source": "/gstack@mini-desktop",
|
||
"rawRef": raw_ref,
|
||
"rawSha256": raw_hash,
|
||
"collectedAt": captured_at,
|
||
"collector": collector,
|
||
})
|
||
self.write_json(wave_dir / "performance.json", {
|
||
"schema": "dogfood-first-frame-performance/2",
|
||
"context": {"host": "mini-desktop", "browser": "Chrome", "browserVersion": "146", "viewport": "390x844@1", "network": "Tailscale staging", "timing": "navigation-commit-to-first-interactive-game-frame"},
|
||
"samples": samples,
|
||
})
|
||
self.write_json(wave_dir / "telemetry.json", {
|
||
"schema": "dogfood-telemetry-completeness/1", "expectedCount": 20, "completeCount": 19,
|
||
"evidenceRef": common_ref,
|
||
})
|
||
self.write_json(wave_dir / "alerts.json", {
|
||
"schema": "dogfood-alert-delivery/1",
|
||
"deliveries": [{"alertId": "alert-1", "receivedAt": captured_at, "recipient": "oncall-1", "externalMessageId": "msg-1", "evidenceRef": common_ref}],
|
||
})
|
||
|
||
start = started_at
|
||
end = start + timedelta(hours=manifest["durationHours"])
|
||
(wave_dir / "oncall.tsv").write_text(
|
||
"start_at\tend_at\tprimary\tbackup\tcontact\n"
|
||
f"{start.isoformat().replace('+00:00', 'Z')}\t{end.isoformat().replace('+00:00', 'Z')}\toncall-1\toncall-2\tinternal-roster\n",
|
||
encoding="utf-8",
|
||
)
|
||
(wave_dir / "feedback.tsv").write_text(
|
||
"id\tcreated_at\treporter_id\trole\tcategory\tseverity\tsummary\tstatus\towner\tevidence_ref\n"
|
||
f"F-1\t{captured_at}\tu-1\tplayer\tux\tP2\tfixture feedback\tCLOSED\tops\t{common_ref}\n",
|
||
encoding="utf-8",
|
||
)
|
||
checklist_items = ["role-permissions", "main-chain", "release-negatives", "manual-play-before-feed"]
|
||
(wave_dir / "manual-acceptance.tsv").write_text(
|
||
"item\tresult\toperator\tchecked_at\tevidence_ref\n" + "".join(
|
||
f"{item}\tPASS\toperator-1\t{captured_at}\t{common_ref}\n" for item in checklist_items
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
self.write_json(wave_dir / "drills.json", {
|
||
"schema": "dogfood-drills/1",
|
||
"drills": [{"action": action, "result": "PASS", "ack": f"ACK-DOGFOOD-{wave}-{action}", "performedAt": captured_at, "operator": "ops-1", "evidenceRef": common_ref} for action in ["stop", "delete", "rollback"]],
|
||
})
|
||
stop_conditions = [
|
||
"security-incident", "cross-account-access", "data-loss", "real-charge", "real-ad-impression",
|
||
"unreviewed-content-in-feed", "generation-success-below-80", "confirmed-false-positive",
|
||
"same-root-cause-blocked-two-users", "telemetry-missing-above-5", "review-backlog-over-4h",
|
||
"acceptance-cost-exceeded",
|
||
]
|
||
self.write_json(wave_dir / "safety-status.json", {
|
||
"schema": "dogfood-safety-status/1", "assessedAt": captured_at, "assessor": "ops-1",
|
||
"conditions": [{"condition": condition, "triggered": False} for condition in stop_conditions],
|
||
})
|
||
|
||
def test_init_wave0_creates_fixed_ledgers_and_scale(self) -> None:
|
||
"""wave0 必须固定为 3 名创作者和 5 名玩家。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
|
||
manifest = json.loads((wave_dir / "manifest.json").read_text(encoding="utf-8"))
|
||
self.assertEqual(manifest["wave"], "wave0")
|
||
self.assertEqual(manifest["creatorCount"], 3)
|
||
self.assertEqual(manifest["playerCount"], 5)
|
||
self.assertTrue((wave_dir / "oncall.tsv").is_file())
|
||
self.assertTrue((wave_dir / "feedback.tsv").is_file())
|
||
self.assertTrue((wave_dir / "p0.tsv").is_file())
|
||
self.assertTrue((wave_dir / "config-identity.json").is_file())
|
||
self.assertTrue((wave_dir / "manual-acceptance.tsv").is_file())
|
||
for name in (
|
||
"rc.json", "snapshot.json", "storage-audit.json", "accounts.json", "role-permissions.json",
|
||
"main-chain.json", "release-negatives.json", "manual-play.json", "performance.json",
|
||
"telemetry.json", "alerts.json", "drills.json", "safety-status.json",
|
||
):
|
||
self.assertTrue((wave_dir / name).is_file(), name)
|
||
self.assertTrue((wave_dir / "attachments").is_dir())
|
||
self.assertTrue((wave_dir / "freezes").is_dir())
|
||
|
||
def test_init_wave1_uses_approved_scale(self) -> None:
|
||
"""wave1 必须固定为累计 10 名创作者和 30 名玩家。"""
|
||
wave0_dir, closed_at = self.close_valid_wave0()
|
||
self.assertTrue((wave0_dir / "close-result.json").is_file())
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=closed_at + timedelta(minutes=1), rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = self.root / "wave1"
|
||
|
||
manifest = json.loads((wave_dir / "manifest.json").read_text(encoding="utf-8"))
|
||
self.assertEqual((manifest["creatorCount"], manifest["playerCount"]), (10, 30))
|
||
self.assertEqual(manifest["wave0CloseResultSha256"], hashlib.sha256((wave0_dir / "close-result.json").read_bytes()).hexdigest())
|
||
final_freeze = wave0_dir / "freezes/2026-07-21.sha256.json"
|
||
self.assertEqual(manifest["wave0FinalFreezeSha256"], hashlib.sha256(final_freeze.read_bytes()).hexdigest())
|
||
|
||
def test_wave1_revalidates_bound_wave0_close_and_final_freeze(self) -> None:
|
||
"""Wave1 开波门必须持续复核 Wave0 回执和最终冻结哈希。"""
|
||
wave0_dir, closed_at = self.close_valid_wave0()
|
||
wave1_started_at = closed_at + timedelta(minutes=1)
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=wave1_started_at, rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave1_dir = self.root / "wave1"
|
||
self.populate_valid_evidence(wave1_dir)
|
||
(wave0_dir / "close-result.json").write_text(
|
||
(wave0_dir / "close-result.json").read_text(encoding="utf-8") + " ",
|
||
encoding="utf-8",
|
||
)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave1_dir, "readiness", now=wave1_started_at)
|
||
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == "wave1.wave0-binding" for code, _ in gate.errors))
|
||
|
||
def test_wave1_init_rejects_wave0_with_p0(self) -> None:
|
||
"""Wave0 收口后出现 P0 时不得初始化 Wave1。"""
|
||
wave0_dir, closed_at = self.close_valid_wave0()
|
||
(wave0_dir / "p0.tsv").write_text(
|
||
"id\tseverity\tstatus\tsummary\tevidence_ref\n"
|
||
"P0-1\tP0\tOPEN\tsecurity\tattachments/browser-proof.png\n",
|
||
encoding="utf-8",
|
||
)
|
||
|
||
result = DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=closed_at + timedelta(minutes=1), rc_commit=self.FIXTURE_RC_COMMIT)
|
||
|
||
self.assertNotEqual(result, 0)
|
||
_, errors = DOGFOOD_OPS.inspect_wave0_close(self.root, closed_at + timedelta(minutes=1))
|
||
self.assertTrue(any(code == "wave1.wave0-p0" for code, _ in errors))
|
||
|
||
def test_wave1_init_rejects_wave0_with_false_positive(self) -> None:
|
||
"""Wave0 存在确认发布假阳时不得初始化 Wave1。"""
|
||
wave0_dir, closed_at = self.close_valid_wave0()
|
||
path = wave0_dir / "safety-status.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
next(item for item in payload["conditions"] if item["condition"] == "confirmed-false-positive")["triggered"] = True
|
||
self.write_json(path, payload)
|
||
|
||
result = DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=closed_at + timedelta(minutes=1), rc_commit=self.FIXTURE_RC_COMMIT)
|
||
|
||
self.assertNotEqual(result, 0)
|
||
_, errors = DOGFOOD_OPS.inspect_wave0_close(self.root, closed_at + timedelta(minutes=1))
|
||
self.assertTrue(any(code == "wave1.wave0-false-positive" for code, _ in errors))
|
||
|
||
def test_wave1_init_rejects_wave0_with_review_backlog(self) -> None:
|
||
"""Wave0 存在超过四小时待审积压时不得初始化 Wave1。"""
|
||
wave0_dir, closed_at = self.close_valid_wave0()
|
||
path = wave0_dir / "safety-status.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
next(item for item in payload["conditions"] if item["condition"] == "review-backlog-over-4h")["triggered"] = True
|
||
self.write_json(path, payload)
|
||
|
||
result = DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=closed_at + timedelta(minutes=1), rc_commit=self.FIXTURE_RC_COMMIT)
|
||
|
||
self.assertNotEqual(result, 0)
|
||
_, errors = DOGFOOD_OPS.inspect_wave0_close(self.root, closed_at + timedelta(minutes=1))
|
||
self.assertTrue(any(code == "wave1.wave0-review-backlog" for code, _ in errors))
|
||
|
||
def test_repeated_init_is_idempotent_and_does_not_overwrite(self) -> None:
|
||
"""重复初始化应成功返回,但不得覆盖已经填写的证据。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
feedback = wave_dir / "feedback.tsv"
|
||
feedback.write_text(feedback.read_text(encoding="utf-8") + "F-1\t2026-07-22T00:00:00Z\tu1\tplayer\tbug\tP1\t卡住\tOPEN\tops\tattachments/f.txt\n", encoding="utf-8")
|
||
before = hashlib.sha256(feedback.read_bytes()).hexdigest()
|
||
|
||
result = self.run_tool(
|
||
"init", "--root", str(self.root), "--wave", "wave0",
|
||
"--rc-commit", self.FIXTURE_RC_COMMIT,
|
||
)
|
||
|
||
self.assertEqual(result.returncode, 0, result.stderr)
|
||
self.assertEqual(hashlib.sha256(feedback.read_bytes()).hexdigest(), before)
|
||
self.assertIn("INIT_ALREADY_EXISTS", result.stdout)
|
||
|
||
def test_repeated_init_repairs_missing_templates_without_overwrite(self) -> None:
|
||
"""初始化中断后重试应补回缺失模板,同时保留已经填写的账本。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
feedback = wave_dir / "feedback.tsv"
|
||
feedback.write_text(feedback.read_text(encoding="utf-8") + "kept\n", encoding="utf-8")
|
||
(wave_dir / "rc.json").unlink()
|
||
(wave_dir / "attachments").rmdir()
|
||
|
||
result = self.run_tool(
|
||
"init", "--root", str(self.root), "--wave", "wave0",
|
||
"--rc-commit", self.FIXTURE_RC_COMMIT,
|
||
)
|
||
|
||
self.assertEqual(result.returncode, 0, result.stderr)
|
||
self.assertIn("INIT_REPAIRED", result.stdout)
|
||
self.assertTrue((wave_dir / "rc.json").is_file())
|
||
self.assertTrue((wave_dir / "attachments").is_dir())
|
||
self.assertTrue(feedback.read_text(encoding="utf-8").endswith("kept\n"))
|
||
|
||
def test_init_rejects_unknown_wave_instead_of_creating_a_path(self) -> None:
|
||
"""波次名不得被当成任意相对路径使用。"""
|
||
result = self.run_tool("init", "--root", str(self.root), "--wave", "../wave0")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertFalse((Path(self.temp_dir.name) / "wave0").exists())
|
||
self.assertIn("invalid choice", result.stderr)
|
||
|
||
def test_readiness_fails_closed_when_evidence_is_missing(self) -> None:
|
||
"""仅初始化模板不得被误判为可开波。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("rc.missing", result.stdout)
|
||
self.assertNotIn("GATE_PASS", result.stdout)
|
||
|
||
def test_readiness_accepts_complete_structured_evidence(self) -> None:
|
||
"""完整且互相对账的结构化证据应通过开波门。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||
self.assertIn("GATE_PASS phase=readiness", result.stdout)
|
||
|
||
def test_performance_requires_at_least_30_cold_independent_samples(self) -> None:
|
||
"""冷样本少于 30 个时必须直接失败。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
payload["samples"] = payload["samples"][:29]
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.sample-count", result.stdout)
|
||
|
||
def test_performance_p75_uses_nearest_rank_and_rejects_3000ms(self) -> None:
|
||
"""30 样本的第 23 个值是 P75,恰好 3000ms 不满足严格小于。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
for index, sample in enumerate(payload["samples"]):
|
||
self.set_trace_duration(wave_dir, sample, 1000 if index < 22 else 3000)
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.p75", result.stdout)
|
||
|
||
def test_performance_accepts_p75_of_2999ms(self) -> None:
|
||
"""严格阈值内的第 23 个值应通过。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
for index, sample in enumerate(payload["samples"]):
|
||
self.set_trace_duration(wave_dir, sample, 1000 if index < 22 else 2999)
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||
|
||
def test_performance_rejects_hot_cache_mixed_into_cold_samples(self) -> None:
|
||
"""冷样本文件中混入热缓存记录时不得计算 P75。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
payload["samples"][0]["cache"] = "hot"
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.cold-only", result.stdout)
|
||
|
||
def test_performance_rejects_non_finite_raw_trace_timestamp(self) -> None:
|
||
"""原始 trace 的 NaN 时间戳不得绕过 P75 阈值。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
for sample in payload["samples"]:
|
||
self.set_trace_duration(wave_dir, sample, float("nan"))
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.raw-record", result.stdout)
|
||
|
||
def test_performance_rejects_tampered_raw_record_hash(self) -> None:
|
||
"""原始 trace 内容变化但样本哈希未同步时必须失败。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
performance = json.loads((wave_dir / "performance.json").read_text(encoding="utf-8"))
|
||
raw_path = wave_dir / performance["samples"][0]["rawRef"]
|
||
raw_path.write_text(raw_path.read_text(encoding="utf-8") + " ", encoding="utf-8")
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.raw-record.hash-mismatch", result.stdout)
|
||
|
||
def test_performance_recomputes_har_timestamps(self) -> None:
|
||
"""HAR 样本也必须从原始起止时间重算,不读取 durationMs。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
sample = payload["samples"][0]
|
||
old_path = wave_dir / sample["rawRef"]
|
||
old_path.unlink()
|
||
har_ref = "attachments/perf-00.har"
|
||
har_path = wave_dir / har_ref
|
||
self.write_json(har_path, {
|
||
"log": {
|
||
"pages": [{
|
||
"id": sample["navigationId"],
|
||
"_gstack": {
|
||
"collector": sample["collector"],
|
||
"collectedAt": sample["collectedAt"],
|
||
"navigationCommitAt": "2026-07-23T00:00:00Z",
|
||
"firstInteractiveGameFrameAt": "2026-07-23T00:00:02.999Z",
|
||
},
|
||
}],
|
||
},
|
||
})
|
||
sample["rawRef"] = har_ref
|
||
sample["rawSha256"] = hashlib.sha256(har_path.read_bytes()).hexdigest()
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||
|
||
def test_performance_rejects_collector_identity_mismatch_with_raw_record(self) -> None:
|
||
"""样本声明的采集器身份必须与原始记录内身份完全一致。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
payload["samples"][0]["collector"]["sessionId"] = "forged-session"
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("performance.raw-record", result.stdout)
|
||
|
||
def test_performance_rejects_missing_or_future_collection_time(self) -> None:
|
||
"""每条原始记录都必须绑定合法且不在未来的采集时间。"""
|
||
for collected_at in (None, "2999-01-01T00:00:00Z"):
|
||
with self.subTest(collected_at=collected_at):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "performance.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
payload["samples"][0]["collectedAt"] = collected_at
|
||
self.write_json(path, payload)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == "performance.collected-at" for code, _ in gate.errors))
|
||
|
||
def test_main_chain_requires_distinct_evidence_modalities(self) -> None:
|
||
"""同一个占位附件不得同时冒充截图、网络、日志和数据库证据。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "main-chain.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
for field in ("networkRef", "logRef", "databaseRef"):
|
||
payload["stages"][0][field] = payload["stages"][0]["screenshotRef"]
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("main-chain.evidence-modalities", result.stdout)
|
||
|
||
def test_telemetry_accepts_exactly_95_percent_and_rejects_below(self) -> None:
|
||
"""完整率 95% 可过,低于 95% 必须失败。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
telemetry = wave_dir / "telemetry.json"
|
||
|
||
pass_result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
self.assertEqual(pass_result.returncode, 0, pass_result.stdout + pass_result.stderr)
|
||
|
||
self.write_json(telemetry, {"schema": "dogfood-telemetry-completeness/1", "expectedCount": 100, "completeCount": 94, "evidenceRef": "attachments/browser-proof.png"})
|
||
fail_result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
self.assertNotEqual(fail_result.returncode, 0)
|
||
self.assertIn("telemetry.completeness", fail_result.stdout)
|
||
|
||
def test_top_level_pass_cannot_forge_restore_evidence(self) -> None:
|
||
"""写一个 PASS 字符串不能替代隔离恢复目标和证据。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
self.write_json(wave_dir / "snapshot.json", {"schema": "dogfood-snapshot/1", "result": "PASS"})
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("snapshot.mysql", result.stdout)
|
||
self.assertIn("snapshot.isolated-restore", result.stdout)
|
||
|
||
def test_evidence_reference_cannot_escape_wave_directory(self) -> None:
|
||
"""即使目录外文件存在,证据引用也不得越过波次根。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
outside = wave_dir.parent / "outside.png"
|
||
outside.write_text("outside", encoding="utf-8")
|
||
payload = json.loads((wave_dir / "release-negatives.json").read_text(encoding="utf-8"))
|
||
payload["checks"][0]["evidenceRef"] = "../outside.png"
|
||
self.write_json(wave_dir / "release-negatives.json", payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("reference.outside-wave", result.stdout)
|
||
|
||
def test_main_chain_game_artifact_must_match_runtime_package(self) -> None:
|
||
"""游戏产物哈希必须与实际 runtime package 内容一致。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
path = wave_dir / "main-chain.json"
|
||
payload = json.loads(path.read_text(encoding="utf-8"))
|
||
payload["identity"]["gameArtifactSha256"] = "f" * 64
|
||
self.write_json(path, payload)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("main-chain.game-artifact", result.stdout)
|
||
|
||
def test_main_chain_game_artifact_must_match_review_and_database_records(self) -> None:
|
||
"""审核记录或主链数据库任一身份漂移都必须拒绝。"""
|
||
for ref_key in ("reviewRecordRef", "mainChainDatabaseRef"):
|
||
with self.subTest(ref_key=ref_key):
|
||
with tempfile.TemporaryDirectory() as temp_dir:
|
||
root = Path(temp_dir) / "evidence"
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
root, "wave0", rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
chain = json.loads((wave_dir / "main-chain.json").read_text(encoding="utf-8"))
|
||
binding_path = wave_dir / chain["identity"][ref_key]
|
||
binding = json.loads(binding_path.read_text(encoding="utf-8"))
|
||
binding["gameArtifactSha256"] = "f" * 64
|
||
self.write_json(binding_path, binding)
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "readiness")
|
||
self.assertNotEqual(gate.run(), 0)
|
||
self.assertTrue(any(code == "main-chain.game-artifact" for code, _ in gate.errors))
|
||
|
||
def test_game_database_binding_must_be_used_by_main_chain_stage(self) -> None:
|
||
"""独立伪造的数据库绑定不能替代主链阶段实际采集的数据库证据。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
self.populate_valid_evidence(wave_dir)
|
||
chain_path = wave_dir / "main-chain.json"
|
||
chain = json.loads(chain_path.read_text(encoding="utf-8"))
|
||
original = wave_dir / chain["identity"]["mainChainDatabaseRef"]
|
||
detached_ref = "attachments/detached-database-binding.json"
|
||
(wave_dir / detached_ref).write_bytes(original.read_bytes())
|
||
chain["identity"]["mainChainDatabaseRef"] = detached_ref
|
||
self.write_json(chain_path, chain)
|
||
|
||
result = self.run_tool("check", "--wave-dir", str(wave_dir), "--phase", "readiness")
|
||
|
||
self.assertNotEqual(result.returncode, 0)
|
||
self.assertIn("main-chain.game-artifact", result.stdout)
|
||
|
||
def test_close_requires_freeze_and_detects_post_freeze_tampering(self) -> None:
|
||
"""收口门必须验证最近一次 SHA-256 冻结,冻结后改动立即失效。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
closed_at = started_at + timedelta(hours=24, minutes=1)
|
||
|
||
missing = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=closed_at)
|
||
self.assertNotEqual(missing.run(), 0)
|
||
self.assertTrue(any(code == "freeze.missing" for code, _ in missing.errors))
|
||
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-20", now=started_at.replace(hour=23)), 0)
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-21", now=closed_at), 0)
|
||
passed = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=closed_at)
|
||
self.assertEqual(passed.run(), 0, passed.errors)
|
||
self.assertTrue((wave_dir / "close-result.json").is_file())
|
||
|
||
with (wave_dir / "feedback.tsv").open("a", encoding="utf-8") as stream:
|
||
stream.write("F-2\t2026-07-22T08:00:00Z\tu-2\tplayer\tbug\tP1\ttampered\tOPEN\tops\tattachments/browser-proof.png\n")
|
||
tampered = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=closed_at)
|
||
self.assertNotEqual(tampered.run(), 0)
|
||
self.assertTrue(any(code == "freeze.hash-mismatch" for code, _ in tampered.errors))
|
||
|
||
def test_close_rejects_malformed_freeze_without_traceback(self) -> None:
|
||
"""畸形冻结清单应形成可追踪失败,而不是让闸门异常退出。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
freeze_dir = wave_dir / "freezes"
|
||
(freeze_dir / "2026-07-20.sha256.json").write_text("[]\n", encoding="utf-8")
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=started_at + timedelta(hours=24))
|
||
result = gate.run()
|
||
|
||
self.assertEqual(result, 1)
|
||
self.assertTrue(any(code == "freeze.schema" for code, _ in gate.errors))
|
||
|
||
def test_close_rejects_symlink_added_after_freeze_without_traceback(self) -> None:
|
||
"""冻结后加入符号链接时应 fail-closed,并保持稳定退出码。"""
|
||
started_at = datetime(2026, 7, 20, 9, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.populate_valid_evidence(wave_dir)
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-20", now=started_at.replace(hour=23)), 0)
|
||
(wave_dir / "attachments" / "late-link").symlink_to(wave_dir / "attachments" / "browser-proof.png")
|
||
|
||
gate = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=started_at + timedelta(hours=24))
|
||
result = gate.run()
|
||
|
||
self.assertEqual(result, 1)
|
||
self.assertTrue(any(code == "freeze.collection" for code, _ in gate.errors))
|
||
|
||
def test_wave1_close_requires_three_chained_daily_freezes(self) -> None:
|
||
"""Wave1 必须真实运行 72 小时并覆盖四个实际 UTC 日期,且哈希链不可篡改。"""
|
||
_, wave0_closed_at = self.close_valid_wave0()
|
||
started_at = wave0_closed_at + timedelta(minutes=1)
|
||
self.assertEqual(DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave1", now=started_at, rc_commit=self.FIXTURE_RC_COMMIT), 0)
|
||
wave_dir = self.root / "wave1"
|
||
self.populate_valid_evidence(wave_dir)
|
||
close_time = started_at + timedelta(hours=72, minutes=1)
|
||
for offset in range(4):
|
||
freeze_now = datetime.combine(
|
||
started_at.date() + timedelta(days=offset),
|
||
datetime.min.time(),
|
||
tzinfo=timezone.utc,
|
||
) + timedelta(hours=23)
|
||
if freeze_now > close_time:
|
||
freeze_now = close_time
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, freeze_now.date().isoformat(), now=freeze_now), 0)
|
||
passed = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=close_time)
|
||
self.assertEqual(passed.run(), 0, passed.errors)
|
||
|
||
first_path = sorted((wave_dir / "freezes").glob("*.sha256.json"))[0]
|
||
first_path.write_text(first_path.read_text(encoding="utf-8") + " ", encoding="utf-8")
|
||
tampered = DOGFOOD_OPS.EvidenceGate(wave_dir, "close", now=close_time)
|
||
self.assertNotEqual(tampered.run(), 0)
|
||
self.assertTrue(any(code == "freeze.chain" for code, _ in tampered.errors))
|
||
|
||
def test_record_drill_requires_exact_ack_and_is_idempotent(self) -> None:
|
||
"""演练记录必须使用精确 ACK,重复签收同一证据不得制造重复行。"""
|
||
wave_dir = self.init_wave("wave0")
|
||
evidence_ref, _ = self.add_attachment(wave_dir, "stop-drill.txt")
|
||
common_args = (
|
||
"record-drill", "--wave-dir", str(wave_dir), "--action", "stop",
|
||
"--operator", "ops-1", "--performed-at", "2026-07-22T06:00:00Z",
|
||
"--evidence-ref", evidence_ref,
|
||
)
|
||
|
||
refused = self.run_tool(*common_args, "--ack", "yes")
|
||
self.assertNotEqual(refused.returncode, 0)
|
||
self.assertIn("ACK_REFUSED", refused.stderr)
|
||
|
||
for invalid_time in ("2026-07-20T08:59:59Z", "2999-01-01T00:00:00Z"):
|
||
invalid_args = list(common_args)
|
||
invalid_args[invalid_args.index("--performed-at") + 1] = invalid_time
|
||
refused_time = self.run_tool(*invalid_args, "--ack", "ACK-DOGFOOD-wave0-stop")
|
||
self.assertNotEqual(refused_time.returncode, 0)
|
||
self.assertIn("DRILL_REFUSED", refused_time.stderr)
|
||
|
||
accepted = self.run_tool(*common_args, "--ack", "ACK-DOGFOOD-wave0-stop")
|
||
self.assertEqual(accepted.returncode, 0, accepted.stdout + accepted.stderr)
|
||
repeated = self.run_tool(*common_args, "--ack", "ACK-DOGFOOD-wave0-stop")
|
||
self.assertEqual(repeated.returncode, 0, repeated.stdout + repeated.stderr)
|
||
self.assertIn("DRILL_ALREADY_RECORDED", repeated.stdout)
|
||
payload = json.loads((wave_dir / "drills.json").read_text(encoding="utf-8"))
|
||
self.assertEqual(len(payload["drills"]), 1)
|
||
|
||
def test_freeze_is_idempotent_but_refuses_same_day_rewrite(self) -> None:
|
||
"""同一证据可重复冻结,内容变化后不得覆盖同日清单。"""
|
||
now = datetime(2026, 7, 22, 8, 0, tzinfo=timezone.utc)
|
||
DOGFOOD_OPS.initialize_wave(
|
||
self.root, "wave0", now=now, rc_commit=self.FIXTURE_RC_COMMIT)
|
||
wave_dir = self.root / "wave0"
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-22", now=now), 0)
|
||
self.assertEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-22", now=now + timedelta(minutes=1)), 0)
|
||
|
||
(wave_dir / "feedback.tsv").write_text("changed\n", encoding="utf-8")
|
||
self.assertNotEqual(DOGFOOD_OPS.freeze_evidence(wave_dir, "2026-07-22", now=now + timedelta(minutes=2)), 0)
|
||
|
||
def test_shell_entrypoints_delegate_to_the_single_cli(self) -> None:
|
||
"""值班入口应直接调用同一套实现,避免脚本间口径漂移。"""
|
||
scripts = [
|
||
"init-wave.sh", "check-readiness.sh", "check-close.sh",
|
||
"freeze-evidence.sh", "record-drill.sh",
|
||
]
|
||
for name in scripts:
|
||
result = subprocess.run(
|
||
["bash", str(OPS_DIR / name), "--help"],
|
||
check=False,
|
||
text=True,
|
||
capture_output=True,
|
||
)
|
||
self.assertEqual(result.returncode, 0, f"{name}: {result.stdout}{result.stderr}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|