P28-01-RESTART: 現HEADの性能基準:途中保存・別PID再開の再現

This commit is contained in:
Agent Control Runner
2026-10-21 03:50:39 +09:00
parent e524ff2924
commit 954528a7c5
3 changed files with 76 additions and 1 deletions
@@ -314,6 +314,8 @@ def json_sha(value):
# and Python. Fixed decimal form is valid JSON; keys and strings remain verbatim.
def encode(v):
if isinstance(v, dict):
require(len({str(k) for k in v}) == len(v), "canonical_key_collision")
require(all(type(k) is str for k in v), "canonical JSON requires text keys")
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + encode(v[k])
for k in sorted(v)) + "}"
if isinstance(v, list):
@@ -335,6 +337,7 @@ def check_sidecar(public, sidecar):
"sidecar root/missing")
check_no_metrics(public)
full = sidecar["report"]
require(full.get("stop_reason") != "canonical_key_collision", "canonical_key_collision")
require(public == redact_report(full), "public redaction differs from W2")
restored = copy.deepcopy(public)
for path, payload in payload_paths(restored).items():
@@ -19,6 +19,7 @@ var boundary: Variant = null
var at_390_event_verbatim: Array = []
var at_390_pending: Array = []
var redaction_ok := true
var canonical_key_collision := false
const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SAVE_LOADED",
"SAVE_LOAD_FAILED", "SAVE_MIGRATION_SUCCEEDED", "SAVE_MIGRATION_FAILED",
"SAVE_RECOVERY_LOADED", "SAVE_RECOVERY_CANDIDATE_LOADED", "SAVE_RECOVERY_FAILED"]
@@ -26,7 +27,13 @@ const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SA
func canonical_json(value: Variant) -> String:
if value is Dictionary:
var keys: Array = value.keys()
keys.sort()
keys.sort_custom(func(a, b): return str(a) < str(b))
for index: int in range(1, keys.size()):
if str(keys[index - 1]) == str(keys[index]):
canonical_key_collision = true
# Unwind serialization before emitting failure evidence.
call_deferred("finish", "canonical_key_collision")
return ""
var parts: PackedStringArray = []
for key: Variant in keys:
parts.append(JSON.stringify(str(key)) + ":" + canonical_json(value[key]))
@@ -567,6 +574,8 @@ func public_has_forbidden(value: Variant) -> bool:
func finish(reason: String) -> void:
if finished:
return
if canonical_key_collision:
reason = "canonical_key_collision"
finished = true
report["stop_reason"] = reason
report["key_forms"] = key_forms
@@ -252,6 +252,69 @@ class RestartTests(unittest.TestCase):
self.assertIn("test_restart.py", self.inventory_before)
self.assertEqual(self.here_inventory(), self.inventory_before)
def actual_canonical_sidecar(self):
relative = Path("native-e524ff2-canonical-failure-r01/report_full.json")
roots = [check.ROOT / "build",
check.ROOT.parent / ".runtime" / "P28-01-RESTART" / "build"]
if os.environ.get("BUILD_OUTPUT_DIR"):
roots.insert(0, Path(os.environ["BUILD_OUTPUT_DIR"]))
path = next((root / relative for root in roots if (root / relative).is_file()), None)
self.assertIsNotNone(path, "required retained native-e524ff2 sidecar is missing")
raw = path.read_bytes()
self.addCleanup(lambda: self.assertEqual(path.read_bytes(), raw))
return json.loads(raw)
def test_z1_actual_text_key_order_hash(self):
sidecar = self.actual_canonical_sidecar()
events = sidecar["at_390_event_verbatim"]
self.assertEqual(len(events), 16)
first = events[0]
self.assertEqual(first["event_id"], "domain_event.000000000009")
self.assertEqual(first["absolute_minute"], 362)
self.assertEqual(first["payload"],
{"old_state": 1, "reason": "ENTRY_COMPLETED", "state": 2})
self.assertEqual(check.json_sha(first),
"9ca80bec0c605ecd288f7671952d30fe66c67f291c5a618ea54623f07394edb9")
for key in ("raw_event_sha256", "canonical_sha256"):
self.assertEqual(sidecar["report"]["at_390"]["events"][0][key],
"f28afa0899d7efea59ab99ae06276ba354779a73cac705fa4801162f27c592d7")
with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"):
check.check_sidecar(check.redact_report(sidecar["report"]), sidecar)
def test_z1_all_actual_event_bindings(self):
original = self.actual_canonical_sidecar()
sidecar = copy.deepcopy(original)
full = sidecar["report"]
self.assertEqual(len(sidecar["at_390_event_verbatim"]), 16)
self.assertEqual(len(full["at_390"]["events"]), 16)
for event, summary in zip(sidecar["at_390_event_verbatim"], full["at_390"]["events"]):
summary["raw_event_sha256"] = check.json_sha(event)
summary["canonical_sha256"] = check.json_sha(
check.canonical_event(event, full["event_id"]))
self.assertEqual(sidecar["at_390_event_verbatim"], original["at_390_event_verbatim"])
self.assertEqual(check.check_sidecar(check.redact_report(full), sidecar), full)
for index in range(16):
with self.subTest(index=index):
changed = copy.deepcopy(sidecar)
changed["report"]["at_390"]["events"][index]["raw_event_sha256"] = "0" * 64
with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"):
check.check_sidecar(check.redact_report(changed["report"]), changed)
def test_z2_collision_rejected(self):
for ambiguous in ({1: "numeric", "1": "text"},
{"nested": [{1: "numeric", "1": "text"}]}):
with self.subTest(value=ambiguous), self.assertRaisesRegex(
ValueError, "canonical_key_collision"):
check.json_sha(ambiguous)
full = report()
full["stop_reason"] = "canonical_key_collision"
public, sidecar = evidence(full)
with self.assertRaisesRegex(ValueError, "canonical_key_collision"):
check.check_report(public, sidecar)
public["stop_reason"] = "complete"
with self.assertRaises(ValueError):
check.check_report(public, sidecar)
def rejects(self, original, mutations, validator):
for name, mutation in mutations.items():
with self.subTest(rejection=name):