From 0880f70e934dde6bdf9aff639c0312061898dd4d Mon Sep 17 00:00:00 2001 From: G-Fourteen Date: Sat, 1 Nov 2025 11:26:40 -0600 Subject: [PATCH] Build AI assets in workflows and expand tests --- .github/workflows/main-branch.yml | 16 +++--- .github/workflows/pull-request.yml | 2 + build.py | 81 ++++++++++++++++++++++++++++++ tests/conftest.py | 24 +++++++++ tests/test_auto_launch.py | 24 ++++----- tests/test_build_artifacts.py | 30 +++++++++++ 6 files changed, 154 insertions(+), 23 deletions(-) create mode 100644 build.py create mode 100644 tests/conftest.py create mode 100644 tests/test_build_artifacts.py diff --git a/.github/workflows/main-branch.yml b/.github/workflows/main-branch.yml index 6f54e25..8ce9f36 100644 --- a/.github/workflows/main-branch.yml +++ b/.github/workflows/main-branch.yml @@ -13,18 +13,14 @@ jobs: steps: - name: Check out repository uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" - name: Configure GitHub Pages uses: actions/configure-pages@v5 - name: Prepare static site bundle - run: | - rm -rf dist - mkdir -p dist - cp index.html dist/ - cp landing.js dist/ - cp style.css dist/ - cp -R AI dist/AI - cp -R tests dist/tests - cp ai-instruct.txt dist/ + run: python build.py --output dist - name: Upload build artifacts uses: actions/upload-artifact@v4 with: @@ -55,6 +51,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Build static site bundle + run: python build.py --output dist - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ac03505..291fce1 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -20,6 +20,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.11" + - name: Build static site bundle + run: python build.py --output dist - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/build.py b/build.py new file mode 100644 index 0000000..4f472ca --- /dev/null +++ b/build.py @@ -0,0 +1,81 @@ +"""Build the Talk to Unity static site bundle. + +This script prepares the distributable assets by copying the landing +experience, the AI application, and supporting files into a target +output directory. The workflow jobs call this script to ensure both +pages are included before running tests or publishing the bundle. +""" + +from __future__ import annotations + +import argparse +import shutil +from pathlib import Path + +ROOT = Path(__file__).resolve().parent + +FILES_TO_COPY = [ + "index.html", + "landing.js", + "style.css", + "ai-instruct.txt", +] + +DIRECTORIES_TO_COPY = [ + "AI", + "tests", +] + + +def build(output_dir: str | Path = "dist") -> Path: + """Build the static site bundle into ``output_dir``. + + Args: + output_dir: Destination directory for the build. Relative paths + are resolved from the repository root. + + Returns: + The absolute :class:`Path` to the created output directory. + """ + + destination = Path(output_dir) + if not destination.is_absolute(): + destination = ROOT / destination + + if destination.exists(): + shutil.rmtree(destination) + destination.mkdir(parents=True) + + for file_name in FILES_TO_COPY: + source = ROOT / file_name + if not source.exists(): + raise FileNotFoundError(f"Missing required file: {source}") + shutil.copy2(source, destination / source.name) + + for directory in DIRECTORIES_TO_COPY: + source_dir = ROOT / directory + if not source_dir.exists(): + raise FileNotFoundError(f"Missing required directory: {source_dir}") + shutil.copytree(source_dir, destination / directory) + + return destination + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Build the Talk to Unity static site bundle.") + parser.add_argument( + "--output", + default="dist", + help="Destination directory for build artifacts (default: dist)", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + output_path = build(args.output) + print(f"Built Talk to Unity site in {output_path}") + + +if __name__ == "__main__": + main() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1cdc90d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +import build + + +@pytest.fixture(scope="session") +def built_site(tmp_path_factory: pytest.TempPathFactory) -> Path: + output_dir = tmp_path_factory.mktemp("site") + build.build(output_dir) + return output_dir + + +@pytest.fixture(scope="session") +def app_js(built_site: Path) -> str: + return (built_site / "AI" / "app.js").read_text(encoding="utf-8") diff --git a/tests/test_auto_launch.py b/tests/test_auto_launch.py index 56fd592..65280b1 100644 --- a/tests/test_auto_launch.py +++ b/tests/test_auto_launch.py @@ -1,18 +1,14 @@ -from pathlib import Path import re -ROOT = Path(__file__).resolve().parents[1] -APP_JS = (ROOT / "AI" / "app.js").read_text() +def test_auto_start_without_landing_section(app_js: str) -> None: + assert "const shouldAutoStartExperience = !launchButton && !landingSection;" in app_js + assert "if (shouldAutoStartExperience)" in app_js + assert "void startApplication();" in app_js -def test_auto_start_without_landing_section(): - assert "const shouldAutoStartExperience = !launchButton && !landingSection;" in APP_JS - assert "if (shouldAutoStartExperience)" in APP_JS - assert "void startApplication();" in APP_JS - -def test_app_js_defines_expected_functions(): - function_names = set(re.findall(r"function[ \t]+(\w+)", APP_JS)) +def test_app_js_defines_expected_functions(app_js: str) -> None: + function_names = set(re.findall(r"function[ \t]+(\w+)", app_js)) expected = { "formatDependencyList", "setStatusMessage", @@ -65,16 +61,16 @@ def test_app_js_defines_expected_functions(): assert not missing, f"Missing functions from app.js: {missing}" -def test_start_application_sets_app_state(): - match = re.search(r"async function startApplication\([^)]*\)\s*{(.*?)}\s*async function", APP_JS, re.S) +def test_start_application_sets_app_state(app_js: str) -> None: + match = re.search(r"async function startApplication\([^)]*\)\s*{(.*?)}\s*async function", app_js, re.S) assert match, "startApplication definition not found" body = match.group(1) assert "appStarted = true;" in body or "appStarted = !0" in body assert "bodyElement.dataset.appState = 'experience';" in body -def test_set_muted_state_announces_changes(): - match = re.search(r"async function setMutedState\([^)]*\)\s*{(.*?)}\s*async function", APP_JS, re.S) +def test_set_muted_state_announces_changes(app_js: str) -> None: + match = re.search(r"async function setMutedState\([^)]*\)\s*{(.*?)}\s*async function", app_js, re.S) assert match, "setMutedState definition not found" body = match.group(1) assert "isMuted = muted;" in body or "isMuted = true;" in body diff --git a/tests/test_build_artifacts.py b/tests/test_build_artifacts.py new file mode 100644 index 0000000..dac70ac --- /dev/null +++ b/tests/test_build_artifacts.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + + +def test_landing_assets_are_copied(built_site: Path) -> None: + for asset in ("index.html", "landing.js", "style.css", "ai-instruct.txt"): + asset_path = built_site / asset + assert asset_path.exists(), f"Missing landing asset in build output: {asset_path}" + + +def test_ai_application_assets_are_copied(built_site: Path) -> None: + ai_dir = built_site / "AI" + assert (ai_dir / "index.html").exists(), "AI index.html missing from build output" + assert (ai_dir / "app.js").exists(), "AI app.js missing from build output" + assert (ai_dir / "ai.css").exists(), "AI stylesheet missing from build output" + + +def test_ai_application_page_includes_new_redirect(built_site: Path) -> None: + ai_index = (built_site / "AI" / "index.html").read_text(encoding="utf-8") + assert "checks-passed=true" in ai_index + assert "Unity Voice Lab" in ai_index + assert '' in ai_index + + +def test_ai_styles_reflect_refined_layout(built_site: Path) -> None: + ai_css = (built_site / "AI" / "ai.css").read_text(encoding="utf-8") + assert "body[data-app-state='experience'] .voice-stage" in ai_css + assert "border-radius" in ai_css + assert "compatibility-notice" in ai_css