migration: prove standalone maintenance and correct visual evidence
This commit is contained in:
@@ -254,7 +254,7 @@
|
|||||||
"code_hotspots": [
|
"code_hotspots": [
|
||||||
{
|
{
|
||||||
"path": "frontend/styles/styles.css",
|
"path": "frontend/styles/styles.css",
|
||||||
"bytes": 361780,
|
"bytes": 361776,
|
||||||
"lines": 15465
|
"lines": 15465
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -279,12 +279,12 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/app.js",
|
"path": "frontend/app.js",
|
||||||
"bytes": 91151,
|
"bytes": 89213,
|
||||||
"lines": 1939
|
"lines": 1939
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/heaven/page.js",
|
"path": "frontend/pages/heaven/page.js",
|
||||||
"bytes": 88322,
|
"bytes": 86493,
|
||||||
"lines": 1830
|
"lines": 1830
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -299,7 +299,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/features/heaven/service.py",
|
"path": "backend/features/heaven/service.py",
|
||||||
"bytes": 64421,
|
"bytes": 63123,
|
||||||
"lines": 1303
|
"lines": 1303
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "frontend/pages/market/runtime.js",
|
"path": "frontend/pages/market/runtime.js",
|
||||||
"bytes": 57053,
|
"bytes": 55720,
|
||||||
"lines": 1333
|
"lines": 1333
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -319,7 +319,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "backend/application.py",
|
"path": "backend/application.py",
|
||||||
"bytes": 50934,
|
"bytes": 49784,
|
||||||
"lines": 1165
|
"lines": 1165
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -329,7 +329,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "database.py",
|
"path": "database.py",
|
||||||
"bytes": 34013,
|
"bytes": 33284,
|
||||||
"lines": 746
|
"lines": 746
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ RETIRED_FRONTEND_SOURCE_RANGES = (
|
|||||||
(9022, 9027),
|
(9022, 9027),
|
||||||
(9052, 9055),
|
(9052, 9055),
|
||||||
)
|
)
|
||||||
|
AUDITED_FRONTEND_SOURCE_LINE_COUNT = 9283
|
||||||
|
|
||||||
|
|
||||||
def sha256(path: Path) -> str:
|
def sha256(path: Path) -> str:
|
||||||
@@ -51,12 +52,10 @@ def reassembled_frontend_runtime() -> str:
|
|||||||
assembled.append(content)
|
assembled.append(content)
|
||||||
next_line = end + 1
|
next_line = end + 1
|
||||||
|
|
||||||
original_line_count = len(
|
if next_line != AUDITED_FRONTEND_SOURCE_LINE_COUNT + 1:
|
||||||
(ORIGINAL_STATIC / "app.js").read_text(encoding="utf-8").splitlines()
|
|
||||||
)
|
|
||||||
if next_line != original_line_count + 1:
|
|
||||||
raise AssertionError(
|
raise AssertionError(
|
||||||
f"app.js source coverage ended at {next_line - 1}, expected {original_line_count}"
|
"app.js source coverage ended at "
|
||||||
|
f"{next_line - 1}, expected {AUDITED_FRONTEND_SOURCE_LINE_COUNT}"
|
||||||
)
|
)
|
||||||
return "".join(assembled)
|
return "".join(assembled)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from tests.preservation_helpers import reassembled_frontend_runtime
|
from tests.preservation_helpers import reassembled_frontend_runtime
|
||||||
|
|
||||||
@@ -46,6 +47,14 @@ class FrontendBoundaryTests(unittest.TestCase):
|
|||||||
self.assertIn("const state = window.XiaobaiState.create({", app)
|
self.assertIn("const state = window.XiaobaiState.create({", app)
|
||||||
self.assertNotIn("const state = {", app)
|
self.assertNotIn("const state = {", app)
|
||||||
|
|
||||||
|
def test_candidate_runtime_reassembly_does_not_require_original_static(self) -> None:
|
||||||
|
with patch(
|
||||||
|
"tests.preservation_helpers.ORIGINAL_STATIC",
|
||||||
|
ROOT / "missing-original-static",
|
||||||
|
):
|
||||||
|
app = reassembled_frontend_runtime()
|
||||||
|
self.assertIn("function openView(", app)
|
||||||
|
|
||||||
def test_runtime_page_registry_matches_governance_registry(self) -> None:
|
def test_runtime_page_registry_matches_governance_registry(self) -> None:
|
||||||
expected = json.loads(
|
expected = json.loads(
|
||||||
(ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
|
(ROOT / "config" / "pages.config.json").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from tools.build_api_registry import build as build_api_registry
|
from tools.build_api_registry import build as build_api_registry
|
||||||
from tools.build_architecture_inventory import build as build_architecture_inventory
|
from tools.build_architecture_inventory import (
|
||||||
|
build as build_architecture_inventory,
|
||||||
|
source_metrics,
|
||||||
|
)
|
||||||
|
from tools.verify_baseline import python_test_command
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
@@ -34,6 +39,22 @@ class MaintenanceToolTests(unittest.TestCase):
|
|||||||
self.assertIn("stop_e2e_server(server)", source)
|
self.assertIn("stop_e2e_server(server)", source)
|
||||||
self.assertNotIn('"static/app.js"', source)
|
self.assertNotIn('"static/app.js"', source)
|
||||||
|
|
||||||
|
def test_standalone_verifier_excludes_only_migration_comparison_modules(self) -> None:
|
||||||
|
repository_command = python_test_command(preservation_baseline=True)
|
||||||
|
standalone_command = python_test_command(preservation_baseline=False)
|
||||||
|
self.assertIn("discover", repository_command)
|
||||||
|
self.assertTrue(any(item == "tests.test_frontend_contract" for item in standalone_command))
|
||||||
|
self.assertFalse(any("test_preservation_" in item for item in standalone_command))
|
||||||
|
|
||||||
|
def test_architecture_metrics_are_independent_of_checkout_line_endings(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
lf = root / "lf.js"
|
||||||
|
crlf = root / "crlf.js"
|
||||||
|
lf.write_bytes(b"const a = 1;\nconst b = 2;\n")
|
||||||
|
crlf.write_bytes(b"const a = 1;\r\nconst b = 2;\r\n")
|
||||||
|
self.assertEqual(source_metrics(lf), source_metrics(crlf))
|
||||||
|
|
||||||
def test_every_tool_has_a_non_mutating_help_path(self) -> None:
|
def test_every_tool_has_a_non_mutating_help_path(self) -> None:
|
||||||
for path in sorted((ROOT / "tools").glob("*.py")):
|
for path in sorted((ROOT / "tools").glob("*.py")):
|
||||||
if path.name.startswith("_"):
|
if path.name.startswith("_"):
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ maintenance command cannot be mistaken for a historical migration rewrite.
|
|||||||
- `python tools/build_architecture_inventory.py [--check]`: generate or verify
|
- `python tools/build_architecture_inventory.py [--check]`: generate or verify
|
||||||
`config/architecture-inventory.json` from the candidate source tree.
|
`config/architecture-inventory.json` from the candidate source tree.
|
||||||
|
|
||||||
|
Inside the canonical `webapp/app/` checkout, `verify_baseline.py` runs the full preservation
|
||||||
|
suite against the retained original baseline and enforces `git diff --check`. In a standalone
|
||||||
|
`app/` export where that baseline and Git checkout do not exist, the same command runs all
|
||||||
|
candidate-owned tests, skips only `test_preservation_*` comparison modules, and reports the Git
|
||||||
|
check as skipped. Product, registry, JavaScript, database, and optional Playwright checks remain
|
||||||
|
active in both modes.
|
||||||
|
|
||||||
## Acceptance and differential checks
|
## Acceptance and differential checks
|
||||||
|
|
||||||
- `run_preservation_runtime.py`: start an isolated original or candidate runtime with an
|
- `run_preservation_runtime.py`: start an isolated original or candidate runtime with an
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ def css_layers(html: str) -> list[str]:
|
|||||||
return re.findall(r'<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"', html)
|
return re.findall(r'<link[^>]+rel="stylesheet"[^>]+href="([^"]+)"', html)
|
||||||
|
|
||||||
|
|
||||||
|
def source_metrics(path: Path) -> dict[str, int]:
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
return {
|
||||||
|
"bytes": len(text.encode("utf-8")),
|
||||||
|
"lines": len(text.splitlines()),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def code_hotspots() -> list[dict[str, Any]]:
|
def code_hotspots() -> list[dict[str, Any]]:
|
||||||
candidates = [
|
candidates = [
|
||||||
"backend/application.py",
|
"backend/application.py",
|
||||||
@@ -99,13 +107,7 @@ def code_hotspots() -> list[dict[str, Any]]:
|
|||||||
path = ROOT / name
|
path = ROOT / name
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
continue
|
continue
|
||||||
rows.append(
|
rows.append({"path": name, **source_metrics(path)})
|
||||||
{
|
|
||||||
"path": name,
|
|
||||||
"bytes": path.stat().st_size,
|
|
||||||
"lines": len(path.read_text(encoding="utf-8").splitlines()),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return sorted(rows, key=lambda item: item["bytes"], reverse=True)
|
return sorted(rows, key=lambda item: item["bytes"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,39 @@ def run(label: str, command: list[str]) -> None:
|
|||||||
subprocess.run(command, cwd=ROOT, check=True)
|
subprocess.run(command, cwd=ROOT, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def python_test_command(preservation_baseline: bool | None = None) -> list[str]:
|
||||||
|
if preservation_baseline is None:
|
||||||
|
preservation_baseline = (ROOT.parent / "static" / "app.js").is_file()
|
||||||
|
if preservation_baseline:
|
||||||
|
return [sys.executable, "-m", "unittest", "discover", "-s", "tests"]
|
||||||
|
modules = [
|
||||||
|
f"tests.{path.stem}"
|
||||||
|
for path in sorted((ROOT / "tests").glob("test_*.py"))
|
||||||
|
if not path.stem.startswith("test_preservation_")
|
||||||
|
]
|
||||||
|
if not modules:
|
||||||
|
raise RuntimeError("no standalone candidate tests found")
|
||||||
|
return [sys.executable, "-m", "unittest", *modules]
|
||||||
|
|
||||||
|
|
||||||
|
def verify_git_diff() -> None:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "rev-parse", "--show-toplevel"],
|
||||||
|
cwd=ROOT,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("\n[patch] skipped: standalone export is not a Git checkout")
|
||||||
|
return
|
||||||
|
repository_root = Path(result.stdout.strip()).resolve()
|
||||||
|
if ROOT != repository_root / "app":
|
||||||
|
print("\n[patch] skipped: standalone export is outside the canonical app path")
|
||||||
|
return
|
||||||
|
run("patch", ["git", "diff", "--check"])
|
||||||
|
|
||||||
|
|
||||||
def verify_database() -> None:
|
def verify_database() -> None:
|
||||||
database = ROOT / "data" / "review.db"
|
database = ROOT / "data" / "review.db"
|
||||||
if not database.exists():
|
if not database.exists():
|
||||||
@@ -97,7 +130,7 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
run("python", [sys.executable, "-m", "unittest", "discover", "-s", "tests"])
|
run("python", python_test_command())
|
||||||
run(
|
run(
|
||||||
"api-registry",
|
"api-registry",
|
||||||
[sys.executable, "tools/build_api_registry.py", "--check"],
|
[sys.executable, "tools/build_api_registry.py", "--check"],
|
||||||
@@ -117,7 +150,7 @@ def main() -> int:
|
|||||||
"javascript",
|
"javascript",
|
||||||
[node, "--check", script.relative_to(ROOT).as_posix()],
|
[node, "--check", script.relative_to(ROOT).as_posix()],
|
||||||
)
|
)
|
||||||
run("patch", ["git", "diff", "--check"])
|
verify_git_diff()
|
||||||
verify_database()
|
verify_database()
|
||||||
|
|
||||||
if args.e2e:
|
if args.e2e:
|
||||||
|
|||||||
Reference in New Issue
Block a user