P24-02: 操作と通知の全受入
This commit is contained in:
+189
@@ -0,0 +1,189 @@
|
||||
# Repair4 JSON order correction — static phase ready
|
||||
|
||||
Task P24-02; commit `9641ed52def2ff8e5b0222449a4a83a1191e1d37`. Same repair4; no new task, permission change or commit. Root prelaunch proof trusted; no ps, external search, engine, OS, full or final checker executed. Root performs normal submission separately. This is static readiness only, never acceptance.
|
||||
|
||||
Only the P24 operations marker row and suite policy key/object were moved from index 2 to the end of their respective containers. Raw entry bodies retained verbatim; existing main entries and their order retained. Both JSON files parse. Before/after structural comparison passed: all values deep-equal, marker rows remain 180, suite policy key set unchanged (48). Only the specified entry position differs.
|
||||
|
||||
JSON SHA256 before → after:
|
||||
|
||||
- completion_marker_contracts.json: `1653c4beac791ec5aa0a6691275ecf5c798ffd689482747c739d7eb2ea5f87d1` → `1063e578eefe6da1b38c24f4c004669bcac67a21884636fb1b9c7e96e4ecf388`
|
||||
- suite-policy.json: `fa395a4d2a98bec607dfa1a030122efe62e9412fccf8dfba2e2db5579561a7b9` → `adc2fbac2463bfdb047bede0182c66083996b75d2bd542a3b8640672d7ca2400`
|
||||
|
||||
`PYTHONPYCACHEPREFIX="$TMPDIR/p24-phase-pyc" python3 -m py_compile` on tools/check_p24_operations_evidence.py, tools/run_all_tests.py, repair4/test_checker_roster_head.py and repair4/check_repair4_preservation.py: exit 0. Cache prefix applied to that command only; no system settings or permissions changed.
|
||||
|
||||
`check_repair4_preservation.py --write-evidence`: exit 0, SOURCE-READY, runtime NOTRUN, schema checks true, failures empty. Both JSON equals_main_plus_p24_only_additions_in_order=true. main.gd remains 279/279; world_preview.gd remains 179 required/180 reconciled with the one allowed bubble adapter; zero missing, duplicate, unexpected or body issues. `git diff --check`: exit 0.
|
||||
|
||||
9 tests reused without rerun: ROOT_LOCAL_VALIDATION_R02_REUSE_RECEIPT.json records actual run 1cde6bdddd3a4d88818d1c38103aedb8, completed exit 0, 9 tests OK. All three dependency SHA256 values match the unchanged files exactly. Earlier 179 result remains historical and is not relabelled.
|
||||
|
||||
Protected SHA256 guard: recorded before any edit, then recomputed after authorized writes; every file below is byte-identical. This includes all task Root instruction/reference/proof files, both GDScripts, all checker/test Python and shared roster. The identical before/after SHA256 is listed for each:
|
||||
|
||||
- `AGENTS.md`: `8d8dc40211390275f858f322743309a0663c4f2d859780a8b8a08cea1450e8b3`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/NEXT_CLOSURE.md`: `9301fa028052174ae9fa5acf006b50fd6c3f21fcbc6e04fc1a398efc7d9be1cb`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/OWNER_REPAIR4_CODE_ONLY_HANDOFF.md`: `940996894b2ea28cf46b992bd5c2719d4ef952452d0585396eae6664a63e6f53`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/OWNER_REPAIR4_GUI_INSTRUCTION.md`: `003724ef4a597298016a59f6198dec2ca3c94115c88afb7dd6cbd7478e0fe1ae`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/OWNER_REPAIR4_JSON_ORDER_CORRECTION.md`: `1e16de00c419467fdfa87b1bf28c004f579576a97cbb04fa5ac06070697532d5`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/OWNER_REPAIR4_SUPPLEMENT_CHECKER.md`: `1eeb4ac0d14347700a9d6e599ce61e36b3324f5fcc4385266aaf16a1c8ab80bb`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/VERIFIED_PRELAUNCH.json`: `c68f35352233d9adb91b1db203cc0e9d47db3f91ec103b9d37a9eeec578f9ba2`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/original-GUI-copy/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json`: `1653c4beac791ec5aa0a6691275ecf5c798ffd689482747c739d7eb2ea5f87d1`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/original-GUI-copy/scenes/main/main.gd.source.txt`: `f71f8c15493d6b51002ee4afb41cc349efe77e860944c1f79a5b24f928ecbd24`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/original-GUI-copy/scenes/main/world_preview.gd.source.txt`: `8c2daa9e95d56e6e40bb2071b206693069cb7e4731b4d660d59d41c34cb43dfa`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_CODE_PHASE_PRELAUNCH_PROOF/original-GUI-copy/tools/regression/suite-policy.json`: `fa395a4d2a98bec607dfa1a030122efe62e9412fccf8dfba2e2db5579561a7b9`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_LOCAL_VALIDATION_R02_REUSE_RECEIPT.json`: `ded162f334d6d7a7422ee76d03e26d1544658a06242bc2e2f3f5c6913890d524`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/ROOT_ZSH_TEMP_TRANSPORT_FIX.md`: `96f0620efdee9b073fcad279de243111a4d5266335d29a93acb19a5dc1b453b3`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/collector_protocol.json`: `bfe94a37304c54ac615bcbd6f938e79933730298fb1c00c6e7ce1922fda3aec2`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/owner-gui-direction-r04.txt`: `2936e33085ce4eae5f556401a55c781b8ac18208ef48f76cacda5faa97e26585`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py`: `909c8c283aca5ef6ad14485bea405e248280a1a63cb6f2fd3458b32a8632cee3`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/test_checker_roster_head.py`: `e528dc2939d3561ac1d890d46caf29632316f4ed4aeb8f5efdaed59d130b9234`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/P24_ALL8_CURRENT_ACCEPTANCE_AND_NEXT_WORK.json`: `5a7c01fc2d6d37046413fc1f81e89005da11616bd0b97c94e6ea1fe0b7668298`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/P24_EXISTING_LIMITED_EVIDENCE_LOCATION_AND_SHA_AUDIT.json`: `ada4a9d2cf612e7cb0b0528f4b4c09b61fb45d47411ed3e78726138d4d233c48`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/REFERENCE_MANIFEST.json`: `08ae7860410859e504bf74a9484d1003f3f8d5f0e6e057a6268a0742af590301`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/main-source/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json`: `6900df6014431ff160c0d18b0dfa65ef0a3fa78eaa08922eaa96de18d5b9fd8c`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/main-source/scenes/main/main.gd.source.txt`: `92677e4ac57ed8d418a4a716bf0874cbbaea79cff893e5163c82ad1f75e7140a`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/main-source/scenes/main/world_preview.gd.source.txt`: `4bc49a19a38b9c3b4d6bfc243ddf46f1f25469a8273adb5e812911ef9424a1f2`
|
||||
- `docs/plans/original-completion-2026-10-02/execution/p24-02/root-current-main-reference-20261017/main-source/tools/regression/suite-policy.json`: `7eb085d7e7a383785b4faedb503d0f6002bcad431e3188785aef901ce123769f`
|
||||
- `scenes/main/main.gd`: `f71f8c15493d6b51002ee4afb41cc349efe77e860944c1f79a5b24f928ecbd24`
|
||||
- `scenes/main/world_preview.gd`: `8c2daa9e95d56e6e40bb2071b206693069cb7e4731b4d660d59d41c34cb43dfa`
|
||||
- `tools/check_p24_operations_evidence.py`: `3dc04b66ab07e2193595aff4ad63baf1ada732cee02c8b4d67298b933bd21487`
|
||||
- `tools/run_all_tests.py`: `1d56dc16ab34f5a8f77f3ed7e833fb6d9fe73aa37cf297a0597e41bfa9c9e4b4`
|
||||
|
||||
Pending: whole8, Root S/M/T/Q composition, native, current full, whole-unit independent review and Windows. No acceptance index created or edited. Earlier failed preparation and quality-check output is retained below as historical evidence; the authorized corrective checks above supersede only the static readiness result.
|
||||
|
||||
---
|
||||
|
||||
## Historical prior phase status (retained verbatim)
|
||||
|
||||
# Repair4 code-only phase — blocked, no submit
|
||||
|
||||
Task P24-02, actual HEAD `9641ed52def2ff8e5b0222449a4a83a1191e1d37`.
|
||||
The trusted Root bridge's VERIFIED_PRELAUNCH.json was read first. Its lease,
|
||||
original-GUI copies, no-competing-job check and quota record are the launch
|
||||
authority. No bridge duties were repeated. No acceptance is claimed.
|
||||
|
||||
## Code changes
|
||||
|
||||
The evidence checker delegates its required roster to the actual
|
||||
`tools/run_all_tests.py:roster(root)` through importlib, without executing main.
|
||||
It compares exact suite sets and rejects malformed/missing/unknown/duplicate
|
||||
rows, missing paths, nonzero or non-integer failures (including bool), missing
|
||||
or non-True passed, and missing/malformed/mismatched source hashes. Optional
|
||||
counts cannot replace rows and must agree exactly when present.
|
||||
HEAD comes from git in the repository root; both environment variables are
|
||||
cross-checks only. Git failure or either mismatch returns an empty value.
|
||||
The other condition, OS/PID, artifact, profile and pending-template guards
|
||||
were left unchanged. Isolated stdlib tests cover S3's eight negative groups
|
||||
and positive roster/HEAD controls; all temporary Git writes are confined to
|
||||
the disposable test repository.
|
||||
|
||||
The preservation script now emits the requested report on the authorized
|
||||
no-argument invocation, including failures. Its METRIC_IDS regex previously
|
||||
stopped at the type annotation's closing bracket in `Array[StringName]`;
|
||||
it now reads the assigned array. The real pinned source contains affection.
|
||||
No preservation rule or protected file was changed to make a check pass.
|
||||
|
||||
## Actual local command results
|
||||
|
||||
1. Exact authorized `python3 -m py_compile` for the four specified Python
|
||||
files: **exit 1**. Python 3.9 attempted to create its configured cache
|
||||
directory outside the workspace and raised:
|
||||
`PermissionError: [Errno 1] Operation not permitted: '/Users/korizatou114/Library/Caches/com.apple.python/Users/korizatou114/Projects/TheTree-managed-worktrees/p24-02-operations'`.
|
||||
This was not rerun with an environment override or relabelled as passed.
|
||||
2. Exact authorized importlib import command: **exit 0**.
|
||||
3. Exact authorized `test_checker_roster_head.py`: **exit 0**, 9 tests OK.
|
||||
Expected mismatch diagnostics appeared for CANDIDATE_COMMIT/TARGET_HEAD.
|
||||
4. Exact authorized `check_repair4_preservation.py`: initially **exit 1**
|
||||
with two JSON preservation failures and the regex false negative.
|
||||
After fixing the regex, rerun **exit 1**, SOURCE-NOT-READY, runtime NOTRUN,
|
||||
schema checks true. Remaining failure output:
|
||||
|
||||
- `docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json: reconciled JSON != main snapshot + P24-only additions (value/order)`
|
||||
- `tools/regression/suite-policy.json: reconciled JSON != main snapshot + P24-only additions (value/order)`
|
||||
|
||||
Both GDScript checks have zero missing, duplicate, unexpected or lost-body
|
||||
items. JSON entry presence/duplicate checks also pass. The protected JSON
|
||||
files insert the P24 operations row/policy at position 2; the strict
|
||||
three-way expectation appends P24-only additions after main's entries.
|
||||
The contracts have 180 rows, first difference at index 2. Minimal concrete
|
||||
Root correction if retaining this strict order contract: relocate only
|
||||
`tests/ui/test_operations_notification.gd` to the end of `rows` and to the
|
||||
end of `suite_policies`, preserving its values and every main entry/order.
|
||||
These protected edits require a subsequent authorized Root phase. They
|
||||
were not performed here; the strict check and failure report are preserved.
|
||||
5. Exact authorized `shasum -a 256` on the four protected files: **exit 0**;
|
||||
all four match the pre-launch proof:
|
||||
|
||||
| File | SHA256 |
|
||||
| --- | --- |
|
||||
| scenes/main/main.gd | f71f8c15493d6b51002ee4afb41cc349efe77e860944c1f79a5b24f928ecbd24 |
|
||||
| scenes/main/world_preview.gd | 8c2daa9e95d56e6e40bb2071b206693069cb7e4731b4d660d59d41c34cb43dfa |
|
||||
| tools/regression/suite-policy.json | fa395a4d2a98bec607dfa1a030122efe62e9412fccf8dfba2e2db5579561a7b9 |
|
||||
| docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json | 1653c4beac791ec5aa0a6691275ecf5c798ffd689482747c739d7eb2ea5f87d1 |
|
||||
|
||||
Git status already contained four protected GUI modifications, Root
|
||||
instructions/reference/proof files and the unfinished repair4 script before
|
||||
this phase. Those were preserved. This phase's writes are confined to the
|
||||
five authorized paths: checker, preservation script, its result, stdlib test,
|
||||
and this status. The dirty preexisting worktree is not claimed to have only
|
||||
these five paths relative to HEAD. No commit, branch, push or submission was
|
||||
performed. Failed steps 1 and 4 prohibit normal submit; task stays repair_ready.
|
||||
|
||||
## All eight conditions remain unmet
|
||||
|
||||
| Frozen condition | Status | Remaining dependencies |
|
||||
| --- | --- | --- |
|
||||
| placement_and_invalid_drop | NOTRUN | Genuine composition; Root Godot/local tests and actual valid/invalid placement, model/ledger/selection preservation and click evidence. |
|
||||
| modal_and_pause_resume | NOTRUN | Composed runtime; actual modal focus/input isolation, pending command/save identity, pause freeze and exactly-once resume evidence. |
|
||||
| unit_bubbles | NOTRUN | Static schema now passes, but composed runtime must prove actual state/render binding, switching and removal. |
|
||||
| event_log | NOTRUN | Actual composed production event sequence, bounded log, Japanese values/time/reasons and pause/load deduplication evidence. |
|
||||
| important_warning_cutin | NOTRUN | Actual production invasion/duel/defense triggers and upstream/catalog/assets applicability; dismiss/stop/resume runtime evidence. Injected fixtures cannot establish production triggers. |
|
||||
| click_alternatives | NOTRUN | Composed navigation/assignment runtime evidence; accepted scales/bounds applicability. P23 held-gesture manual checks remain deferred, not waived. |
|
||||
| japanese_input_and_scale | NOTRUN | Actual OS Japanese composition/confirmation/cancel, persisted save label and invalid feedback, representative scales/focus/clipping; macOS development evidence and separate same-candidate Windows acceptance. |
|
||||
| final_validation_review | NOTRUN | All prior conditions; Root composition, static/import and affected Godot tests, exact-final full roster, real-index verify-only, independent Claude runtime review, current-main integration and final validation. |
|
||||
|
||||
SOURCE-READY describes only the narrow checker code/isolated test work, not
|
||||
these eight condition groups or the failed preservation gate.
|
||||
|
||||
## Unresolved Root S/M/T/Q prerequisite (S5)
|
||||
|
||||
The old-base submitted S differs from combined current-main T/Q outside the
|
||||
four reconciled files. A real externally bound submitted SHA (S), main context
|
||||
SHA (M), tested tree (T), tested commit (Q), complete blob bindings and clean
|
||||
genuine composed checkout are still required before heavy final evidence.
|
||||
The current P24 checker has no such Git-provenance contract. HEAD checking
|
||||
does not establish composition. No environment or receipt relabelling is a
|
||||
substitute.
|
||||
|
||||
Root must reuse/adapt the accepted P23 composition functions from current-main
|
||||
`tools/check_p23_life_observation_evidence.py`, absent entirely on this worker
|
||||
base. Explicit missing helper/API dependencies are `strict_json`,
|
||||
`marker_join`, `structural_equal`, `verify_marker_join`, `git_bytes`,
|
||||
`blob_oid`, `git_blob_stream`, `verify_composition`, `tested_checkout`,
|
||||
`root_context`, `input_map`, and `input_provenance`, plus the verified-view
|
||||
registry `_VERIFIED_VIEWS` and externally bound role/blob context. P23's
|
||||
marker join is P23-specific and must receive a reviewed P24 adaptation, not
|
||||
be imported unchanged as proof. Root must use current-main
|
||||
`tools/run_all_tests.py:source_refs` and `roster` on that genuine composed
|
||||
checkout for the complete source/settings/QA map and required suite set.
|
||||
The worker's shared roster delegation is implemented; composition is NOTRUN.
|
||||
No missing helper was stubbed or imported into this old-base worker.
|
||||
|
||||
The static report also names these concrete composition resources:
|
||||
`scenes/main/world_preview.gd`, `scripts/ui/event_log_dialog.gd`,
|
||||
`scripts/ui/manual_save_slots_dialog.gd`, and `scripts/ui/room_presentation.gd`
|
||||
(changed-on-main). Root must resolve their complete current-main context;
|
||||
this phase did not copy or edit them.
|
||||
|
||||
## Historical evidence and GUI stop (S6)
|
||||
|
||||
The GUI phase ended on the actual Antigravity individual quota, Error ID
|
||||
`b866fa14-cff8-4fc1-ac4f-fba529d47498-136`; queued followup failed at the same
|
||||
quota (ending 143). No replacement GUI worker or engine was launched.
|
||||
|
||||
The prior 179 passes, legacy 60 evidence, event-log evidence and every earlier
|
||||
pass are stale/inapplicable as acceptance or full regression for this changed
|
||||
P24 source. P23 full179 remains valid only for its original accepted
|
||||
d2581af/M5041b88/treea700519 source and closed P23-03. Original evidence and
|
||||
closure bytes remain unchanged. No old result was transferred to this source.
|
||||
No Godot, native/OS collector, full roster, run_all_tests main, full checker
|
||||
against an index, acceptance fixture/index, or independent review was run.
|
||||
All mandatory runtime, OS, review, integration and Windows gates remain
|
||||
NOTRUN/pending as listed above. Nothing here counts as acceptance.
|
||||
+1621
File diff suppressed because it is too large
Load Diff
+475
@@ -0,0 +1,475 @@
|
||||
#!/usr/bin/env python3
|
||||
"""P24-02 repair4 static overlap/preservation check (SOURCE-READY evidence only).
|
||||
|
||||
This is NOT runtime, import, native, OS or acceptance proof. It only proves,
|
||||
statically and mechanically, that the four reconciled files contain:
|
||||
|
||||
* every scope-qualified GDScript symbol (class_name / class / func / signal /
|
||||
const / enum / class-body var) of the pinned current-main snapshot, and
|
||||
* every symbol the P24 worker diff (base 1a5abc6 -> 9641ed5) added,
|
||||
|
||||
each exactly once in its own scope (so a legitimate ``_init`` or ``_draw`` in a
|
||||
different inner class is never reported as a duplicate), that main's body lines
|
||||
and P24's added body lines survived, that both JSON files equal the exact
|
||||
3-way expectation "main snapshot + P24-only additions" (values and order), and
|
||||
that UI actor bubbles only read fields that exist in the current relation /
|
||||
Staff / Guest schema on main.
|
||||
|
||||
Usage (static, no Godot):
|
||||
python3 docs/plans/original-completion-2026-10-02/execution/p24-02/repair4/check_repair4_preservation.py \
|
||||
[--write-evidence]
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
ROOT = HERE.parents[5]
|
||||
REF_DIR = HERE.parent / "root-current-main-reference-20261017"
|
||||
MANIFEST = REF_DIR / "REFERENCE_MANIFEST.json"
|
||||
EVIDENCE = HERE / "SOURCE_READY_PRESERVATION_RESULT.json"
|
||||
|
||||
BASE = "1a5abc65163097d4b36a060d68f58c92093eb4e9"
|
||||
P24 = "9641ed52def2ff8e5b0222449a4a83a1191e1d37"
|
||||
MAIN = "51e8657bfbe24856186cc6530358c5459cf909b6"
|
||||
|
||||
# Explicit, reviewed repair4 deviations. Anything not listed here fails.
|
||||
REPAIR4_ADDED_SYMBOLS = {
|
||||
"scenes/main/world_preview.gd": {
|
||||
"world_preview.gd::func::_draw_actor_bubble":
|
||||
"P24 bubble binding re-expressed on main's P23 observation pipeline: "
|
||||
"bubbles are drawn per fitted actor row on the LifeOverlay canvas "
|
||||
"without mutating the observation report.",
|
||||
},
|
||||
}
|
||||
# P24-added body lines intentionally re-expressed (not dropped) in repair4.
|
||||
REPAIR4_REEXPRESSED_P24_LINES = {
|
||||
("scenes/main/world_preview.gd", "world_preview.gd::func::_draw_actors"):
|
||||
"main replaced immediate _draw_pixel_actor with observed rows; P24's inline "
|
||||
"bubble calls are re-bound through _draw_actor_bubble(row, canvas) in "
|
||||
"main's _draw_actors(canvas).",
|
||||
("scenes/main/world_preview.gd", "world_preview.gd::func::_draw_unit_bubble"):
|
||||
"same bubble geometry/glyphs, but draw calls go to the LifeOverlay canvas "
|
||||
"(canvas.draw_*) and follow the fitted row origin/scale.",
|
||||
}
|
||||
|
||||
failures: list[str] = []
|
||||
|
||||
|
||||
def fail(msg: str) -> None:
|
||||
failures.append(msg)
|
||||
|
||||
|
||||
def git_show(rev: str, path: str) -> str | None:
|
||||
try:
|
||||
return subprocess.run(["git", "show", f"{rev}:{path}"], cwd=ROOT, check=True,
|
||||
capture_output=True).stdout.decode("utf-8")
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
|
||||
def git_blob(data: bytes) -> str:
|
||||
return hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- GDScript
|
||||
DECL = [
|
||||
("class_name", re.compile(r"^class_name\s+(\w+)")),
|
||||
("class", re.compile(r"^class\s+(\w+)\b")),
|
||||
("func", re.compile(r"^(?:static\s+)?func\s+(\w+)\s*\(")),
|
||||
("signal", re.compile(r"^signal\s+(\w+)")),
|
||||
("const", re.compile(r"^const\s+(\w+)")),
|
||||
("enum", re.compile(r"^enum\s+(\w+)")),
|
||||
("var", re.compile(r"^(?:@\w+(?:\([^)]*\))?\s+)*(?:static\s+)?var\s+(\w+)")),
|
||||
]
|
||||
|
||||
|
||||
def indent_of(line: str) -> int:
|
||||
n = 0
|
||||
for ch in line:
|
||||
if ch == "\t":
|
||||
n += 1
|
||||
elif ch == " ":
|
||||
n += 0.25
|
||||
else:
|
||||
break
|
||||
return int(n)
|
||||
|
||||
|
||||
def gd_symbols(text: str, fname: str) -> list[dict]:
|
||||
"""Scope-qualified declarations with their body line ranges."""
|
||||
lines = text.split("\n")
|
||||
stack: list[tuple[int, str]] = [] # (class indent, qualified scope)
|
||||
out: list[dict] = []
|
||||
in_triple = False
|
||||
for i, raw in enumerate(lines):
|
||||
stripped = raw.strip()
|
||||
if raw.count('"""') % 2 == 1:
|
||||
in_triple = not in_triple
|
||||
continue
|
||||
if in_triple or not stripped or stripped.startswith("#"):
|
||||
continue
|
||||
ind = indent_of(raw)
|
||||
while stack and ind <= stack[-1][0]:
|
||||
stack.pop()
|
||||
scope = stack[-1][1] if stack else fname
|
||||
body_indent = stack[-1][0] + 1 if stack else 0
|
||||
if ind != body_indent:
|
||||
continue
|
||||
for kind, rx in DECL:
|
||||
m = rx.match(stripped)
|
||||
if m:
|
||||
out.append({"id": f"{scope}::{kind}::{m[1]}", "line": i, "indent": ind, "kind": kind})
|
||||
if kind == "class":
|
||||
stack.append((ind, f"{scope}.{m[1]}"))
|
||||
break
|
||||
# body = following lines until next line at indent <= decl indent (non-blank)
|
||||
for sym in out:
|
||||
start, ind = sym["line"], sym["indent"]
|
||||
end = start + 1
|
||||
while end < len(lines):
|
||||
s = lines[end]
|
||||
if s.strip() and not s.strip().startswith("#") and indent_of(s) <= ind:
|
||||
break
|
||||
end += 1
|
||||
sym["body"] = [ln.strip() for ln in lines[start:end] if ln.strip()]
|
||||
return out
|
||||
|
||||
|
||||
def by_id(syms: list[dict]) -> dict[str, dict]:
|
||||
return {s["id"]: s for s in syms}
|
||||
|
||||
|
||||
def check_gd(path: str, main_text: str, report: dict) -> None:
|
||||
fname = Path(path).name
|
||||
base_t, p24_t = git_show(BASE, path), git_show(P24, path)
|
||||
recon_t = (ROOT / path).read_text(encoding="utf-8")
|
||||
if base_t is None or p24_t is None:
|
||||
fail(f"{path}: pinned base/P24 revision unavailable")
|
||||
return
|
||||
base, p24, main, recon = (gd_symbols(t, fname) for t in (base_t, p24_t, main_text, recon_t))
|
||||
B, P, M, R = map(by_id, (base, p24, main, recon))
|
||||
counts = Counter(s["id"] for s in recon)
|
||||
dups = sorted(k for k, c in counts.items() if c > 1)
|
||||
p24_added = sorted(set(P) - set(B))
|
||||
required = sorted(set(M) | set(p24_added))
|
||||
missing = sorted(k for k in required if k not in R)
|
||||
allowed_new = REPAIR4_ADDED_SYMBOLS.get(path, {})
|
||||
unexpected = sorted(k for k in R if k not in required and k not in allowed_new)
|
||||
main_removed = sorted(set(B) - set(M))
|
||||
lost_p24_on_removed = [k for k in main_removed if k in P and P[k]["body"] != B[k]["body"]]
|
||||
for k in dups:
|
||||
fail(f"{path}: duplicate definition {k}")
|
||||
for k in missing:
|
||||
fail(f"{path}: missing symbol {k}")
|
||||
for k in unexpected:
|
||||
fail(f"{path}: unexpected symbol {k}")
|
||||
for k in lost_p24_on_removed:
|
||||
fail(f"{path}: P24 modified {k} but main removed it")
|
||||
# Body preservation (set semantics on stripped lines).
|
||||
body_issues = []
|
||||
for k, m in M.items():
|
||||
if k not in R:
|
||||
continue
|
||||
b_lines = set(B[k]["body"]) if k in B else set()
|
||||
p_lines = set(P[k]["body"]) if k in P else b_lines
|
||||
p24_removed = b_lines - p_lines
|
||||
need = set(m["body"]) - p24_removed
|
||||
lost = sorted(need - set(R[k]["body"]))
|
||||
if lost:
|
||||
body_issues.append({"symbol": k, "side": "main", "lost_lines": lost})
|
||||
fail(f"{path}: main body lines lost in {k}: {len(lost)}")
|
||||
reexpressed = []
|
||||
for k, p in P.items():
|
||||
b_lines = set(B[k]["body"]) if k in B else set()
|
||||
added = set(p["body"]) - b_lines
|
||||
if not added or k not in R:
|
||||
continue
|
||||
lost = sorted(added - set(R[k]["body"]))
|
||||
if not lost:
|
||||
continue
|
||||
reason = REPAIR4_REEXPRESSED_P24_LINES.get((path, k))
|
||||
if reason:
|
||||
reexpressed.append({"symbol": k, "lines": lost, "reason": reason})
|
||||
else:
|
||||
body_issues.append({"symbol": k, "side": "p24", "lost_lines": lost})
|
||||
fail(f"{path}: P24 added body lines lost in {k}: {len(lost)}")
|
||||
report[path] = {
|
||||
"kind": "gdscript",
|
||||
"main_symbols": sorted(M),
|
||||
"p24_side_symbols": sorted(P),
|
||||
"p24_added_symbols": p24_added,
|
||||
"main_removed_vs_base_untouched_by_p24": [k for k in main_removed if k not in lost_p24_on_removed],
|
||||
"required_count": len(required),
|
||||
"reconciled_symbol_count": len(recon),
|
||||
"missing": missing,
|
||||
"duplicates": dups,
|
||||
"unexpected": unexpected,
|
||||
"repair4_added_with_reason": {k: v for k, v in allowed_new.items() if k in R},
|
||||
"body_issues": body_issues,
|
||||
"p24_lines_reexpressed_with_reason": reexpressed,
|
||||
"conflict_markers": bool(re.search(r"^(<<<<<<<|>>>>>>>|\|\|\|\|\|\|\|) ", recon_t, re.M)),
|
||||
}
|
||||
if report[path]["conflict_markers"]:
|
||||
fail(f"{path}: conflict markers present")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- JSON
|
||||
def load_json_strict(text: str, label: str):
|
||||
def hook(pairs):
|
||||
keys = [k for k, _ in pairs]
|
||||
d = Counter(keys)
|
||||
for k, c in d.items():
|
||||
if c > 1:
|
||||
fail(f"{label}: duplicate JSON key {k!r}")
|
||||
return dict(pairs)
|
||||
return json.loads(text, object_pairs_hook=hook)
|
||||
|
||||
|
||||
def row_key(row):
|
||||
return row.get("test") if isinstance(row, dict) else None
|
||||
|
||||
|
||||
def expect3(base, main, p24, path="$"):
|
||||
"""3-way expectation: main value, plus P24-only changes where main kept base."""
|
||||
if isinstance(main, dict) and isinstance(p24, dict) and isinstance(base, dict):
|
||||
out = {}
|
||||
for k in main:
|
||||
if k in p24 and k in base:
|
||||
out[k] = expect3(base[k], main[k], p24[k], f"{path}.{k}")
|
||||
elif k in p24 and k not in base and p24[k] != main[k]:
|
||||
fail(f"{path}.{k}: added by both main and P24 with different values")
|
||||
out[k] = main[k]
|
||||
else:
|
||||
out[k] = main[k]
|
||||
for k in p24:
|
||||
if k not in base and k not in main:
|
||||
out[k] = p24[k]
|
||||
elif k in base and k not in main and p24[k] != base[k]:
|
||||
fail(f"{path}.{k}: P24 changed a key main removed")
|
||||
return out
|
||||
if (isinstance(main, list) and isinstance(p24, list) and isinstance(base, list)
|
||||
and all(row_key(r) for r in main + p24 + base)):
|
||||
bk = {row_key(r): r for r in base}
|
||||
pk = {row_key(r): r for r in p24}
|
||||
out = []
|
||||
for r in main:
|
||||
k = row_key(r)
|
||||
out.append(expect3(bk[k], r, pk[k], f"{path}[{k}]") if k in bk and k in pk else r)
|
||||
main_keys = {row_key(r) for r in main}
|
||||
for r in p24:
|
||||
k = row_key(r)
|
||||
if k not in bk and k not in main_keys:
|
||||
out.append(r)
|
||||
return out
|
||||
if p24 != base and main == base:
|
||||
return p24
|
||||
if p24 != base and main != base and p24 != main:
|
||||
fail(f"{path}: both main and P24 changed scalar value")
|
||||
return main
|
||||
|
||||
|
||||
def ordered_equal(a, b):
|
||||
if isinstance(a, dict) and isinstance(b, dict):
|
||||
return list(a) == list(b) and all(ordered_equal(a[k], b[k]) for k in a)
|
||||
if isinstance(a, list) and isinstance(b, list):
|
||||
return len(a) == len(b) and all(ordered_equal(x, y) for x, y in zip(a, b))
|
||||
return type(a) is type(b) and a == b
|
||||
|
||||
|
||||
def json_entry_keys(obj) -> list[str]:
|
||||
keys = []
|
||||
for k, v in obj.items():
|
||||
keys.append(k)
|
||||
if isinstance(v, dict):
|
||||
keys += [f"{k}/{s}" for s in v]
|
||||
elif isinstance(v, list) and v and all(row_key(r) for r in v):
|
||||
keys += [f"{k}[{row_key(r)}]" for r in v]
|
||||
return keys
|
||||
|
||||
|
||||
def check_json(path: str, main_text: str, report: dict) -> None:
|
||||
base_t, p24_t = git_show(BASE, path), git_show(P24, path)
|
||||
recon_t = (ROOT / path).read_text(encoding="utf-8")
|
||||
if base_t is None or p24_t is None:
|
||||
fail(f"{path}: pinned base/P24 revision unavailable")
|
||||
return
|
||||
base, main, p24 = (load_json_strict(t, f"{path}@{r}") for t, r in
|
||||
((base_t, "base"), (main_text, "main"), (p24_t, "p24")))
|
||||
recon = load_json_strict(recon_t, f"{path}@reconciled")
|
||||
expected = expect3(base, main, p24)
|
||||
ok = ordered_equal(recon, expected)
|
||||
if not ok:
|
||||
fail(f"{path}: reconciled JSON != main snapshot + P24-only additions (value/order)")
|
||||
mk, pk, bk, rk = map(json_entry_keys, (main, p24, base, recon))
|
||||
rc = Counter(rk)
|
||||
p24_added = [k for k in pk if k not in bk]
|
||||
missing = [k for k in list(dict.fromkeys(mk + p24_added)) if k not in rc]
|
||||
dups = sorted(k for k, c in rc.items() if c > 1)
|
||||
for k in missing:
|
||||
fail(f"{path}: missing JSON entry {k}")
|
||||
for k in dups:
|
||||
fail(f"{path}: duplicate JSON entry {k}")
|
||||
report[path] = {
|
||||
"kind": "json",
|
||||
"main_entry_keys": mk,
|
||||
"p24_side_entry_keys": pk,
|
||||
"p24_added_entry_keys": p24_added,
|
||||
"missing": missing,
|
||||
"duplicates": dups,
|
||||
"equals_main_plus_p24_only_additions_in_order": ok,
|
||||
"main_entries_value_identical": all(
|
||||
ordered_equal(recon.get(k), main[k]) for k in main if not isinstance(main[k], (dict, list))),
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- schema
|
||||
def enum_members(text: str, enum: str) -> set[str]:
|
||||
m = re.search(r"enum\s+" + enum + r"\s*\{(.*?)\}", text, re.S)
|
||||
return set(re.findall(r"\b([A-Z_][A-Z0-9_]*)\b", m[1])) if m else set()
|
||||
|
||||
|
||||
def top_vars(text: str) -> set[str]:
|
||||
return set(re.findall(r"^var\s+(\w+)", text, re.M))
|
||||
|
||||
|
||||
def check_bubble_schema(report: dict) -> None:
|
||||
fb = (ROOT / "scripts/ui/operation_feedback.gd").read_text(encoding="utf-8")
|
||||
wp = (ROOT / "scenes/main/world_preview.gd").read_text(encoding="utf-8")
|
||||
srcs = {
|
||||
"relationship_state": "scripts/relationship/relationship_state.gd",
|
||||
"staff_runtime": "scripts/staff/staff_runtime.gd",
|
||||
"guest_runtime": "scripts/guest/guest_runtime.gd",
|
||||
"relationship_service": "scripts/relationship/relationship_service.gd",
|
||||
"staff_service": "scripts/staff/staff_service.gd",
|
||||
"guest_service": "scripts/guest/guest_service.gd",
|
||||
}
|
||||
main_src = {k: git_show(MAIN, p) for k, p in srcs.items()}
|
||||
if any(v is None for v in main_src.values()):
|
||||
fail("schema: main revision source unavailable")
|
||||
return
|
||||
rel_vars = top_vars(main_src["relationship_state"])
|
||||
metric_ids = set(re.findall(r'&"(\w+)"', re.search(r"const METRIC_IDS[^=]*=\s*\[(.*?)\]", main_src["relationship_state"], re.S)[1]))
|
||||
checks = []
|
||||
|
||||
def need(label, cond):
|
||||
checks.append({"check": label, "ok": bool(cond)})
|
||||
if not cond:
|
||||
fail(f"schema: {label}")
|
||||
|
||||
staff_used = set(re.findall(r"StaffRuntime\.State\.(\w+)", fb))
|
||||
guest_used = set(re.findall(r"GuestRuntime\.State\.(\w+)", fb))
|
||||
lock_used = set(re.findall(r"RelationshipState\.RouteLock\.(\w+)", fb))
|
||||
rel_fields = set(re.findall(r"relationship\.(\w+)", fb)) - {"get"}
|
||||
rel_fields |= set(re.findall(r'relationship\.get\(&"(\w+)"', fb))
|
||||
need(f"StaffRuntime.State members {sorted(staff_used)} exist on main",
|
||||
staff_used and staff_used <= enum_members(main_src["staff_runtime"], "State"))
|
||||
need(f"GuestRuntime.State members {sorted(guest_used)} exist on main",
|
||||
guest_used and guest_used <= enum_members(main_src["guest_runtime"], "State"))
|
||||
need(f"RelationshipState.RouteLock members {sorted(lock_used)} exist on main",
|
||||
lock_used and lock_used <= enum_members(main_src["relationship_state"], "RouteLock"))
|
||||
need(f"RelationshipState fields {sorted(rel_fields)} exist on main", rel_fields and rel_fields <= rel_vars)
|
||||
need("relationship metric 'affection' is a main METRIC_ID", "affection" in metric_ids)
|
||||
need("GuestRuntime.patience_remaining exists on main", "patience_remaining" in top_vars(main_src["guest_runtime"]))
|
||||
wp_rel = set(re.findall(r"rel_val\.(\w+)", wp))
|
||||
need(f"world_preview relation lookup fields {sorted(wp_rel)} exist on main", wp_rel and wp_rel <= rel_vars)
|
||||
for svc, meth in (("relationship_service", "get_relationships"), ("staff_service", "get_staff"),
|
||||
("guest_service", "get_guest")):
|
||||
need(f"main {svc}.{meth}() exists", re.search(r"^func\s+" + meth + r"\s*\(", main_src[svc], re.M))
|
||||
for k in ("relationship_state", "staff_runtime", "guest_runtime"):
|
||||
wt = (ROOT / srcs[k]).read_text(encoding="utf-8")
|
||||
need(f"worker {srcs[k]} identical to main (schema not diverged)", wt == main_src[k])
|
||||
report["bubble_relation_schema"] = {"checks": checks, "relationship_state_vars_main": sorted(rel_vars)}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- deps
|
||||
def root_composition_dependencies(report: dict) -> None:
|
||||
ls_main = subprocess.run(["git", "ls-tree", "-r", "--name-only", MAIN], cwd=ROOT,
|
||||
capture_output=True, text=True).stdout.split("\n")
|
||||
main_files = set(filter(None, ls_main))
|
||||
main_classes: dict[str, str] = {}
|
||||
grep = subprocess.run(["git", "grep", "-h", "-E", r"^class_name\s+\w+", MAIN, "--", "*.gd"], cwd=ROOT,
|
||||
capture_output=True, text=True).stdout
|
||||
for m in re.finditer(r"class_name\s+(\w+)", grep):
|
||||
main_classes[m[1]] = "main"
|
||||
worker_classes = set()
|
||||
for p in ROOT.rglob("*.gd"):
|
||||
if ".godot" in p.parts:
|
||||
continue
|
||||
for m in re.finditer(r"^class_name\s+(\w+)", p.read_text(encoding="utf-8", errors="ignore"), re.M):
|
||||
worker_classes.add(m[1])
|
||||
deps = []
|
||||
for path in ("scenes/main/main.gd", "scenes/main/world_preview.gd"):
|
||||
text = (ROOT / path).read_text(encoding="utf-8")
|
||||
for res in sorted(set(re.findall(r'(?:pre)?load\("res://([^"]+)"\)', text))):
|
||||
in_worker = (ROOT / res).exists()
|
||||
in_main = res in main_files
|
||||
changed = None
|
||||
if in_worker and in_main:
|
||||
mt = git_show(MAIN, res)
|
||||
changed = mt is not None and mt != (ROOT / res).read_text(encoding="utf-8", errors="ignore")
|
||||
if (in_main and not in_worker) or changed:
|
||||
deps.append({"file": path, "resource": res, "kind": "main-only" if not in_worker else "changed-on-main"})
|
||||
words = set(re.findall(r"\b([A-Z]\w+)\b", text))
|
||||
for cls in sorted(words & (set(main_classes) - worker_classes)):
|
||||
deps.append({"file": path, "class_name": cls, "kind": "main-only class_name"})
|
||||
report["root_composition_dependencies"] = {
|
||||
"note": "Reconciled files keep main snapshot text; these symbols/resources resolve only on the "
|
||||
"composed current-main tree. Not stubbed, not copied (outside allowed_paths).",
|
||||
"items": deps,
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- main
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--write-evidence", action="store_true")
|
||||
args = ap.parse_args()
|
||||
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
||||
if (manifest["submitted"], manifest["main_reference"], manifest["registered_base_unchanged"]) != (P24, MAIN, BASE):
|
||||
fail("manifest revisions differ from pinned P24/main/base")
|
||||
report: dict = {"pins": []}
|
||||
files: dict = {}
|
||||
for entry in manifest["all4_current_main_files"]:
|
||||
data = (ROOT / entry["reference_path"]).read_bytes()
|
||||
sha, blob = hashlib.sha256(data).hexdigest(), git_blob(data)
|
||||
ok = sha == entry["sha256"] and blob == entry["main_blob"]
|
||||
live = git_show(MAIN, entry["path"])
|
||||
same_as_main_commit = live is not None and live.encode("utf-8") == data
|
||||
report["pins"].append({"path": entry["path"], "sha256": sha, "blob": blob, "pin_ok": ok,
|
||||
"snapshot_equals_main_commit_blob": same_as_main_commit})
|
||||
if not ok:
|
||||
fail(f"pin mismatch: {entry['path']}")
|
||||
files[entry["path"]] = data.decode("utf-8")
|
||||
results: dict = {}
|
||||
for path, text in files.items():
|
||||
(check_gd if path.endswith(".gd") else check_json)(path, text, results)
|
||||
report["files"] = results
|
||||
check_bubble_schema(report)
|
||||
root_composition_dependencies(report)
|
||||
report["status"] = "SOURCE-READY" if not failures else "SOURCE-NOT-READY"
|
||||
report["runtime_status"] = "NOTRUN"
|
||||
report["scope"] = ("Static source preservation only. Not Godot/import/native/OS/full-roster proof and "
|
||||
"not acceptance. Root owns current-main composition and all execution.")
|
||||
report["failures"] = failures
|
||||
EVIDENCE.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
summary = {p: {k: (len(v) if isinstance(v, list) else v) for k, v in r.items()
|
||||
if k in ("missing", "duplicates", "unexpected", "required_count", "reconciled_symbol_count",
|
||||
"equals_main_plus_p24_only_additions_in_order", "body_issues")}
|
||||
for p, r in results.items()}
|
||||
print(json.dumps({"status": report["status"], "runtime_status": "NOTRUN", "files": summary,
|
||||
"schema_checks_ok": all(c["ok"] for c in report.get("bubble_relation_schema", {}).get("checks", [])),
|
||||
"root_composition_dependencies": len(report["root_composition_dependencies"]["items"]),
|
||||
"failures": failures}, ensure_ascii=False, indent=2))
|
||||
return 0 if not failures else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Isolated checker tests; no acceptance index, engine, or full test execution."""
|
||||
import copy
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[6]
|
||||
spec = importlib.util.spec_from_file_location('checker', ROOT / 'tools/check_p24_operations_evidence.py')
|
||||
checker = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(checker)
|
||||
|
||||
|
||||
class CheckerTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name)
|
||||
(self.root / 'tools').mkdir()
|
||||
shutil.copyfile(ROOT / 'tools/run_all_tests.py', self.root / 'tools/run_all_tests.py')
|
||||
(self.root / 'tests').mkdir()
|
||||
for name in ('run_tests.gd', 'test_alpha.gd', 'test_beta.gd'):
|
||||
(self.root / 'tests' / name).write_text('# isolated fixture ' + name)
|
||||
self.rows = [dict(test=p, failures=0, passed=True,
|
||||
source_sha256=checker.compute_sha256(self.root / p))
|
||||
for p in checker.get_required_suite_roster(self.root)]
|
||||
|
||||
def validate(self, rows, **counts):
|
||||
data = dict(test_suite='tests/ui/test_operations_notification.gd', failures=0, checks=1,
|
||||
full_roster=dict(results=rows, **counts))
|
||||
previous = Path.cwd()
|
||||
try:
|
||||
os.chdir(self.root)
|
||||
return checker._validate_condition_artifact_content(
|
||||
'final_validation_review', data, 'json', self.root, {}, {})
|
||||
finally:
|
||||
os.chdir(previous)
|
||||
|
||||
def test_positive_roster(self):
|
||||
for counts in ({}, dict(total_suites=3, suites_count=3, failures=0)):
|
||||
self.assertEqual(self.validate(self.rows, **counts), (True, ''))
|
||||
|
||||
def test_1_invented_replacement(self):
|
||||
rows = copy.deepcopy(self.rows)
|
||||
rows[0]['test'] = 'tests/invented.gd'
|
||||
self.assertFalse(self.validate(rows)[0])
|
||||
self.assertFalse(self.validate(rows + [rows[0]])[0])
|
||||
|
||||
def test_2_duplicate_padding(self):
|
||||
self.assertFalse(self.validate(self.rows[1:] + [self.rows[1]])[0])
|
||||
|
||||
def test_3_unknown_extra(self):
|
||||
self.assertFalse(self.validate(self.rows + [dict(test='invented')])[0])
|
||||
|
||||
def test_4_required_results(self):
|
||||
for field, value in [('failures', 1), ('failures', False), ('failures', '0'),
|
||||
('passed', False), ('passed', 1), ('passed', None), ('failures', None)]:
|
||||
with self.subTest(field=field, value=value):
|
||||
rows = copy.deepcopy(self.rows)
|
||||
if value is None:
|
||||
del rows[0][field]
|
||||
else:
|
||||
rows[0][field] = value
|
||||
self.assertFalse(self.validate(rows)[0])
|
||||
|
||||
def test_5_source_hash(self):
|
||||
for value in (None, 'bad', '0' * 64, 42):
|
||||
with self.subTest(value=value):
|
||||
rows = copy.deepcopy(self.rows)
|
||||
if value is None:
|
||||
del rows[0]['source_sha256']
|
||||
else:
|
||||
rows[0]['source_sha256'] = value
|
||||
self.assertFalse(self.validate(rows)[0])
|
||||
|
||||
def test_6_missing_or_malformed_rows(self):
|
||||
for rows in (None, [], {}, 'missing', [None], [{}]):
|
||||
with self.subTest(rows=rows):
|
||||
self.assertFalse(self.validate(rows, total_suites=3, failures=0)[0])
|
||||
for counts in (dict(total_suites=4), dict(suites_count=True), dict(failures=None)):
|
||||
self.assertFalse(self.validate(self.rows, **counts)[0])
|
||||
|
||||
def init_repo(self):
|
||||
subprocess.run(['git', 'init', '-q', str(self.root)], check=True, capture_output=True)
|
||||
# Only the disposable repo is written; no workspace git mutation.
|
||||
subprocess.run(['git', '-c', 'user.name=Checker Test', '-c', 'user.email=checker@example.invalid',
|
||||
'-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-qm', 'fixture'],
|
||||
cwd=self.root, check=True, capture_output=True)
|
||||
return subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=self.root,
|
||||
check=True, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
def test_7_env_mismatch_and_positive_head(self):
|
||||
head = self.init_repo()
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), head)
|
||||
for name in ('CANDIDATE_COMMIT', 'TARGET_HEAD'):
|
||||
with patch.dict(os.environ, {name: head}):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), head)
|
||||
with patch.dict(os.environ, {name: '1' * 40}):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), '')
|
||||
with patch.dict(os.environ, dict(CANDIDATE_COMMIT=head, TARGET_HEAD='1' * 40)):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), '')
|
||||
|
||||
def test_8_git_unavailable_env_only(self):
|
||||
with patch.dict(os.environ, {'CANDIDATE_COMMIT': '1' * 40}, clear=True):
|
||||
with patch('subprocess.run', side_effect=FileNotFoundError('git unavailable')):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), '')
|
||||
with patch('subprocess.run', return_value=subprocess.CompletedProcess([], 128, '', 'not a repo')):
|
||||
self.assertEqual(checker.get_actual_git_head(self.root), '')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user