Files
pulse/scripts/release_control/release_note_visuals_test.py
T
pulse-triage[bot] 4efa529215 Close release-note qualification input and fixture gaps
Bind the new compiled notes helper and authored fixture in the rootful source manifest. Repair the visual rollback fixture's missing version and align the internal control-plane page with the active release-reliability target, without changing source routing or published packets.

Change-source: pulse-maintainer
2026-10-01 05:51:28 +01:00

212 lines
8.2 KiB
Python

#!/usr/bin/env python3
import importlib.util
import unittest
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("release_note_visuals.py")
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
SPEC = importlib.util.spec_from_file_location("release_note_visuals", MODULE_PATH)
assert SPEC and SPEC.loader
visuals = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(visuals)
RENDERER_PATH = Path(__file__).with_name("render_release_body.py")
RENDERER_SPEC = importlib.util.spec_from_file_location("render_release_body", RENDERER_PATH)
assert RENDERER_SPEC and RENDERER_SPEC.loader
renderer = importlib.util.module_from_spec(RENDERER_SPEC)
RENDERER_SPEC.loader.exec_module(renderer)
def valid_plan():
return {
"schema_version": 1,
"decision": "This settings comparison makes the responsive redesign immediately visible.",
"captures": [
{
"id": "responsive-settings",
"title": "Settings fit smaller screens",
"description": "Controls remain readable without horizontal scrolling.",
"viewport": {"width": 390, "height": 844},
"before": {
"route": "/settings/general",
"steps": [],
"ready": {
"kind": "role",
"role": "heading",
"name": "General",
"exact": True,
"nth": 0,
},
},
"after": {
"route": "/settings/general",
"steps": [
{
"action": "wait",
"locator": {
"kind": "text",
"value": "Appearance",
"exact": True,
"nth": 0,
},
}
],
"ready": {
"kind": "text",
"value": "Appearance",
"exact": True,
"nth": 0,
},
},
}
],
}
class ReleaseNoteVisualPlanTest(unittest.TestCase):
def test_normalizes_a_safe_accessible_capture_plan(self):
plan = visuals.validate_plan(valid_plan())
self.assertEqual(plan["captures"][0]["viewport"], {"width": 390, "height": 844})
self.assertEqual(
visuals.asset_names(plan),
[
"release-note-responsive-settings-before.png",
"release-note-responsive-settings-now.png",
],
)
def test_structured_output_schema_carries_public_and_capture_bounds(self):
schema = visuals.json_schema()
self.assertEqual(schema["properties"]["schema_version"]["type"], "integer")
self.assertIn("decision", schema["required"])
captures = schema["properties"]["captures"]
capture = captures["items"]["properties"]
self.assertEqual(captures["maxItems"], visuals.MAX_CAPTURES)
self.assertEqual(capture["description"]["maxLength"], 240)
self.assertEqual(
capture["after"]["properties"]["steps"]["maxItems"],
visuals.MAX_STEPS,
)
def test_empty_capture_plan_requires_an_explicit_model_decision(self):
with self.assertRaisesRegex(visuals.PlanError, "decision"):
visuals.validate_plan({"schema_version": 1, "captures": []})
plan = visuals.validate_plan(
{
"schema_version": 1,
"decision": "The investigated changes have no meaningful static visual state.",
"captures": [],
}
)
self.assertEqual(plan["captures"], [])
def test_current_only_capture_has_one_asset(self):
raw = valid_plan()
raw["captures"][0]["before"] = None
plan = visuals.validate_plan(raw)
self.assertEqual(
sum(capture["before"] is not None for capture in plan["captures"]),
0,
)
self.assertEqual(
visuals.asset_names(plan),
["release-note-responsive-settings-now.png"],
)
def test_rejects_external_routes_and_arbitrary_selectors(self):
raw = valid_plan()
raw["captures"][0]["after"]["route"] = "https://example.com/"
with self.assertRaisesRegex(visuals.PlanError, "same-origin"):
visuals.validate_plan(raw)
raw = valid_plan()
raw["captures"][0]["after"]["route"] = "/\\\\example.com/"
with self.assertRaisesRegex(visuals.PlanError, "same-origin"):
visuals.validate_plan(raw)
raw = valid_plan()
raw["captures"][0]["after"]["steps"][0]["locator"]["kind"] = "css"
with self.assertRaisesRegex(visuals.PlanError, "kind must be"):
visuals.validate_plan(raw)
def test_requires_visible_content_for_every_capture_state(self):
raw = valid_plan()
del raw["captures"][0]["after"]["ready"]
with self.assertRaisesRegex(visuals.PlanError, "visible content"):
visuals.validate_plan(raw)
def test_rejects_public_punctuation_disallowed_by_release_notes(self):
raw = valid_plan()
raw["captures"][0]["description"] = "Before; now"
with self.assertRaisesRegex(visuals.PlanError, "semicolons"):
visuals.validate_plan(raw)
def test_renders_release_asset_links_as_before_and_now(self):
plan = visuals.validate_plan(valid_plan())
markdown = visuals.render_markdown(plan, "rcourtman/Pulse", "v6.4.0")
self.assertIn("## See the difference", markdown)
self.assertIn("| Before | Now |", markdown)
self.assertIn(
"https://github.com/rcourtman/Pulse/releases/download/v6.4.0/"
"release-note-responsive-settings-before.png",
markdown,
)
self.assertNotIn(";", markdown)
self.assertNotIn("\u2014", markdown)
def test_release_body_accepts_visuals_between_notes_and_installation(self):
notes = "\n".join(
[
"# Pulse v6.4.0 Release Notes",
"",
"Pulse is easier to use across the devices you already carry.",
"",
"## What's improved",
"",
"- **Responsive settings** - Controls now fit smaller screens cleanly.",
]
)
visual_markdown = visuals.render_markdown(
visuals.validate_plan(valid_plan()), "rcourtman/Pulse", "v6.4.0"
).strip()
rollback = renderer.build_rollback_section(
type(
"Args",
(),
{
"version": "6.4.0",
"rollback_target": "v6.3.2",
"rollback_command": "sudo /bin/update --version v6.3.2",
},
)()
)
body = "\n\n".join(
[
notes,
visual_markdown,
renderer.build_installation_section("6.4.0"),
rollback,
]
) + "\n"
self.assertEqual(renderer.validate_release_body_shape(body, "6.4.0"), body)
def test_release_pipeline_captures_uploads_and_verifies_selected_visuals(self):
workflow = (REPOSITORY_ROOT / ".github/workflows/create-release.yml").read_text()
self.assertIn("release_screenshot_plan:", workflow)
self.assertIn("release_note_visuals:", workflow)
self.assertIn("scripts/capture-release-note-visuals.sh", workflow)
self.assertIn('--release-visuals-file "$VISUAL_MARKDOWN_FILE"', workflow)
self.assertIn('release_upload_with_retry "$TAG" "release-note-visuals/${asset_name}"', workflow)
self.assertIn(r'"release-note-\(.id)-now.png"', workflow)
for trigger_name in ("trigger-release.sh", "trigger-stable-patch.sh"):
trigger = (REPOSITORY_ROOT / "scripts" / trigger_name).read_text()
self.assertIn("--rawfile release_screenshot_plan", trigger)
self.assertIn("release_screenshot_plan: $release_screenshot_plan", trigger)
if __name__ == "__main__":
unittest.main()