Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer, web workflow engine and protocol, tooling, docs and task records. Large generated evidence, user-testing archives, build snapshots and imported test assets remain untracked. Co-Authored-By: Claude Code <noreply@anthropic.com>
185 lines
12 KiB
Python
185 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Publish a completed full run with explicitly matched conditional-case receipts."""
|
|
import argparse
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
BASE = ROOT / "tests/golden/WBV2-P1424-001"
|
|
spec = importlib.util.spec_from_file_location("full_smoke", ROOT / "tools/web/run-wbv2-full-smoke.py")
|
|
runner = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(runner)
|
|
write_json, artifact, now = runner.write_json, runner.artifact, runner.now
|
|
|
|
|
|
def load(path):
|
|
return json.loads(path.read_text())
|
|
|
|
|
|
def digest(path):
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("run", type=Path)
|
|
parser.add_argument("--supplement", type=Path, action="append", default=[])
|
|
parser.add_argument("--repair", type=Path, action="append", default=[],
|
|
help="Complete spec receipt after a test-only correction; runtime input drift is rejected")
|
|
args = parser.parse_args()
|
|
run = args.run.resolve()
|
|
original = load(run / "summary.json")
|
|
inputs = load(run / "inputs.json")
|
|
blockers = []
|
|
if original.get("status") in ("RUNNING", "INTERRUPTED") or len(original.get("groups", [])) != 3:
|
|
raise RuntimeError("The main and both dedicated configurations must finish before publication")
|
|
drift = [ref["path"] for ref in inputs if not (ROOT / ref["path"]).is_file() or digest(ROOT / ref["path"]) != ref["sha256"]]
|
|
supplement_rows = {}
|
|
supplement_receipts = []
|
|
repair_rows = {}
|
|
repair_receipts = []
|
|
repair_paths = set()
|
|
for kind, folder in [("supplement", p) for p in args.supplement] + [("repair", p) for p in args.repair]:
|
|
folder = folder.resolve()
|
|
receipt = load(folder / "result.json")
|
|
report = load(folder / "playwright.json")
|
|
if receipt.get("status") != "PASS" or receipt.get("exitCode") != 0 or report.get("errors"):
|
|
raise RuntimeError(f"Supplement did not pass: {folder}")
|
|
if receipt["source"]["sha256"] != digest(ROOT / receipt["source"]["path"]):
|
|
raise RuntimeError(f"Supplement source changed: {folder}")
|
|
for ref in receipt["artifacts"]:
|
|
if digest(ROOT / ref["path"]) != ref["sha256"]:
|
|
raise RuntimeError(f"Supplement artifact changed: {ref['path']}")
|
|
for case in runner.rows(report.get("suites", [])):
|
|
if case["status"] != "expected":
|
|
raise RuntimeError(f"Supplement contains an unpassed case: {case['title']}")
|
|
key = (case["spec"], case["title"])
|
|
target = supplement_rows if kind == "supplement" else repair_rows
|
|
if key in target:
|
|
raise RuntimeError(f"Duplicate supplement: {key}")
|
|
target[key] = {**case, "receipt": artifact(folder / "result.json")}
|
|
if kind == "supplement":
|
|
supplement_receipts.append(artifact(folder / "result.json"))
|
|
else:
|
|
source = receipt["source"]["path"]
|
|
if not source.startswith("web/tests/e2e/") or not source.endswith(".spec.ts") or source not in drift:
|
|
raise RuntimeError("Only changed E2E spec files can use a test-only repair receipt")
|
|
repair_paths.add(source)
|
|
repair_receipts.append({"source": receipt["source"], "receipt": artifact(folder / "result.json"),
|
|
"originalSource": next(ref for ref in inputs if ref["path"] == source)})
|
|
runtime_drift = [name for name in drift if name not in repair_paths]
|
|
if runtime_drift or not original.get("inputsUnchanged"):
|
|
blockers.append({"kind": "INPUT_DRIFT", "paths": runtime_drift,
|
|
"nextAction": "Run a new complete batch on the changed runtime inputs"})
|
|
repaired_specs = {Path(name).name for name in repair_paths}
|
|
for name in repaired_specs:
|
|
previous = [case for group in original["groups"] for case in group.get("tests", []) if case["spec"] == name]
|
|
replacement = [case for (filename, _), case in repair_rows.items() if filename == name]
|
|
if not previous or len(replacement) < len(previous):
|
|
raise RuntimeError(f"Repair must execute the complete spec: {name}")
|
|
unchanged_titles = {case["title"] for case in previous if case["status"] == "expected"}
|
|
if not unchanged_titles.issubset({case["title"] for case in replacement}):
|
|
raise RuntimeError(f"Repair removed a previously passing test: {name}")
|
|
cases = []
|
|
conditional = []
|
|
for group in original["groups"]:
|
|
ref = group.get("report")
|
|
if ref and digest(ROOT / ref["path"]) != ref["sha256"]:
|
|
raise RuntimeError(f"Group report changed: {group['group']}")
|
|
if group.get("exitCode") not in (0, 1) or group.get("errors") or group.get("status") not in ("PASS", "INCOMPLETE", "FAILED"):
|
|
blockers.append({"kind": "GROUP_FAILURE", "group": group["group"], "status": group["status"],
|
|
"nextAction": "Inspect the group's JSON errors and failure attachments"})
|
|
if group.get("exitCode") == 1:
|
|
failed_in_group = [case for case in group.get("tests", []) if case["status"] not in ("expected", "skipped")]
|
|
if not failed_in_group or any(case["spec"] not in repaired_specs for case in failed_in_group):
|
|
blockers.append({"kind": "UNRESOLVED_GROUP_EXIT", "group": group["group"],
|
|
"nextAction": "Resolve every failing spec or the non-test runner error"})
|
|
for case in group.get("tests", []):
|
|
if case["spec"] in repaired_specs:
|
|
continue
|
|
key = (case["spec"], case["title"])
|
|
if case["status"] == "skipped" and key in supplement_rows:
|
|
replacement = supplement_rows.pop(key)
|
|
conditional.append({"spec": case["spec"], "title": case["title"],
|
|
"originalStatus": "skipped", "supplement": replacement["receipt"]})
|
|
cases.append(replacement)
|
|
else:
|
|
cases.append(case)
|
|
cases.extend(repair_rows.values())
|
|
if supplement_rows:
|
|
raise RuntimeError("Supplement cases must match a collected conditional skip exactly")
|
|
failed = [case for case in cases if case["status"] != "expected"]
|
|
blockers.extend({"kind": "TEST_NOT_PASSED", **case,
|
|
"nextAction": "Repair the concrete failing workflow using its test and local Blender source"} for case in failed)
|
|
expected = set(original["expectedSpecs"])
|
|
observed = {case["spec"] for case in cases}
|
|
if expected != observed:
|
|
blockers.append({"kind": "SPEC_COLLECTION_GAP", "missing": sorted(expected - observed),
|
|
"unexpected": sorted(observed - expected), "nextAction": "Supply the missing configuration or fixture and execute it"})
|
|
catalog = load(ROOT / "web/app/src/capabilities/blender-feature-route-catalog.json")
|
|
soak_path = run / "main/soak.json"
|
|
soak = load(soak_path) if soak_path.is_file() else {}
|
|
if soak.get("profile") != "FORMAL" or soak.get("status") != "READY" or soak.get("actualDurationMs", 0) < 1_800_000:
|
|
blockers.append({"kind": "FORMAL_SOAK_INCOMPLETE", "nextAction": "Complete the formal 30-minute editing and recovery test"})
|
|
report = {
|
|
"schemaVersion": 1, "task": "WBV2-P1424-001", "updatedAt": now(),
|
|
"scope": "Complete Chromium E2E batch plus both dedicated configurations and explicitly configured conditional cases",
|
|
"status": "PASS" if not blockers else "FAILED", "fullDesktopParity": "NOT_ESTABLISHED",
|
|
"totalSpecs": len(expected), "passedSpecs": sum(all(c["status"] == "expected" for c in cases if c["spec"] == name) for name in expected & observed),
|
|
"totalTests": len(cases), "passedTests": sum(c["status"] == "expected" for c in cases),
|
|
"failedTests": len(failed), "unresolvedSkips": sum(c["status"] == "skipped" for c in failed),
|
|
"batch": artifact(run / "summary.json"), "inputs": artifact(run / "inputs.json"),
|
|
"conditionalCases": conditional, "supplements": supplement_receipts,
|
|
"testOnlyRepairs": repair_receipts,
|
|
"formalSoak": {"receipt": artifact(soak_path) if soak else None,
|
|
**{key: soak.get(key) for key in ("actualDurationMs", "cycles", "autosaves", "reopens",
|
|
"finalSnapshotCount", "pageErrors", "observed", "limits")}},
|
|
"historical": {"totalSpecs": 243, "passed": 228, "failed": 13, "skippedConfigurations": 2},
|
|
"blockers": blockers,
|
|
"sourceInventory": {"unitCount": catalog["unitCount"],
|
|
"verifiedUnits": catalog["behavioralEvidence"]["verifiedUnits"],
|
|
"unverifiedUnits": catalog["behavioralEvidence"]["unverifiedUnits"],
|
|
"sourceTreeSha256": catalog["sourceTreeSha256"], "boundUnitDelta": 0,
|
|
"freshness": "Existing catalog; smoke results do not increase source-unit coverage"},
|
|
"limitations": ["Conditional cases retain their original skipped result and an explicit configured passing receipt.",
|
|
"Test-only repairs replace complete spec results; original failures and both test-source hashes remain recorded.",
|
|
"Offline coverage excludes server-native services and file://.",
|
|
"Passing this E2E suite is not proof of complete Blender desktop parity."],
|
|
}
|
|
report_path = BASE / "full-smoke-closure-report.json"
|
|
write_json(report_path, report)
|
|
latest_path = BASE / "full-smoke-latest.json"
|
|
write_json(latest_path, {"status": report["status"], "report": artifact(report_path),
|
|
"batch": report["batch"], "rawBatchStatus": original["status"]})
|
|
product_path = BASE / "product-implementation-report.json"
|
|
product = load(product_path)
|
|
product.setdefault("historicalFullE2ESmoke", product.get("fullE2ESmoke"))
|
|
product["fullE2ESmoke"] = {"status": report["status"], "countUnit": "spec_files", "total": report["totalSpecs"],
|
|
"passed": report["passedSpecs"], "failed": report["totalSpecs"] - report["passedSpecs"],
|
|
"testsTotal": report["totalTests"], "testsPassed": report["passedTests"],
|
|
"unresolvedSkips": report["unresolvedSkips"], "conditionalCases": len(conditional),
|
|
"report": artifact(report_path)}
|
|
write_json(product_path, product)
|
|
status_path = ROOT / "docs/status/WBV2-P1424-001.md"
|
|
lines = [line for line in status_path.read_text().splitlines() if not line.startswith("- full Chromium batch:")]
|
|
lines.append(f"- full Chromium batch: {report['status']}; {report['passedSpecs']}/{report['totalSpecs']} spec files, "
|
|
f"{report['passedTests']}/{report['totalTests']} tests; {len(conditional)} conditional cases explicitly supplemented; "
|
|
"`tests/golden/WBV2-P1424-001/full-smoke-closure-report.json`")
|
|
status_path.write_text("\n".join(lines) + "\n")
|
|
manifest_path = BASE / "manifest.json"
|
|
manifest = load(manifest_path)
|
|
for path in (report_path, latest_path, product_path, status_path, Path(__file__).resolve(),
|
|
ROOT / "tools/web/run-wbv2-full-smoke.py", ROOT / "tools/web/run-wbv2-smoke-closure.py",
|
|
BASE / "full-smoke-task-report.md"):
|
|
ref = artifact(path)
|
|
manifest["artifacts"][ref["path"]] = {"sha256": ref["sha256"], "bytes": path.stat().st_size}
|
|
write_json(manifest_path, manifest)
|
|
print(json.dumps({k: report[k] for k in ("status", "totalSpecs", "passedSpecs", "totalTests", "passedTests", "blockers")}), flush=True)
|
|
return 0 if not blockers else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|