diff --git a/.github/workflows/sync-extension.yml b/.github/workflows/sync-extension.yml new file mode 100644 index 0000000..3aae02a --- /dev/null +++ b/.github/workflows/sync-extension.yml @@ -0,0 +1,126 @@ +name: Sync Extension from sentience-chrome + +on: + repository_dispatch: + types: [extension-updated] + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag from sentience-chrome (e.g., v1.0.0)' + required: true + type: string + schedule: + # Check for new releases daily at 2 AM UTC + - cron: '0 2 * * *' + +jobs: + sync-extension: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout sdk-python + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Determine release tag + id: release + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + TAG="${{ github.event.inputs.release_tag }}" + elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then + TAG="${{ github.event.client_payload.release_tag }}" + else + # Scheduled check - get latest release + TAG=$(curl -s https://api.github.com/repos/${{ secrets.SENTIENCE_CHROME_REPO }}/releases/latest | jq -r '.tag_name // empty') + fi + + if [ -z "$TAG" ] || [ "$TAG" == "null" ]; then + echo "No release found, skipping" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "tag=$TAG" >> $GITHUB_OUTPUT + echo "Release tag: $TAG" + + - name: Download extension files + if: steps.release.outputs.skip != 'true' + run: | + TAG="${{ steps.release.outputs.tag }}" + REPO="${{ secrets.SENTIENCE_CHROME_REPO }}" + + # Download release assets + mkdir -p extension-temp + cd extension-temp + + # Download each file from release + curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" \ + "https://api.github.com/repos/$REPO/releases/tags/$TAG" | \ + jq -r '.assets[] | select(.name | endswith(".js") or endswith(".wasm") or endswith(".json") or endswith(".d.ts")) | .browser_download_url' | \ + while read url; do + filename=$(basename "$url") + curl -L -H "Authorization: token ${{ secrets.SENTIENCE_CHROME_TOKEN }}" "$url" -o "$filename" + done + + # Alternative: Download from release archive if available + # Or use the extension-package artifact + + - name: Copy extension files + if: steps.release.outputs.skip != 'true' + run: | + # Create extension directory structure + mkdir -p sentience/extension/pkg + + # Copy extension files + cp extension-temp/manifest.json sentience/extension/ 2>/dev/null || echo "manifest.json not found in release" + cp extension-temp/content.js sentience/extension/ 2>/dev/null || echo "content.js not found in release" + cp extension-temp/background.js sentience/extension/ 2>/dev/null || echo "background.js not found in release" + cp extension-temp/injected_api.js sentience/extension/ 2>/dev/null || echo "injected_api.js not found in release" + + # Copy WASM files + cp extension-temp/pkg/sentience_core.js sentience/extension/pkg/ 2>/dev/null || echo "sentience_core.js not found" + cp extension-temp/pkg/sentience_core_bg.wasm sentience/extension/pkg/ 2>/dev/null || echo "sentience_core_bg.wasm not found" + cp extension-temp/pkg/*.d.ts sentience/extension/pkg/ 2>/dev/null || echo "Type definitions not found" + + - name: Check for changes + if: steps.release.outputs.skip != 'true' + id: changes + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add sentience/extension/ || true + if git diff --staged --quiet; then + echo "changed=false" >> $GITHUB_OUTPUT + echo "No changes detected" + else + echo "changed=true" >> $GITHUB_OUTPUT + echo "Changes detected" + fi + + - name: Create Pull Request + if: steps.release.outputs.skip != 'true' && steps.changes.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "chore: sync extension files from sentience-chrome ${{ steps.release.outputs.tag }}" + title: "Sync Extension: ${{ steps.release.outputs.tag }}" + body: | + This PR syncs extension files from sentience-chrome release ${{ steps.release.outputs.tag }}. + + **Files updated:** + - Extension manifest and scripts + - WASM binary and bindings + + **Source:** [sentience-chrome release ${{ steps.release.outputs.tag }}](${{ secrets.SENTIENCE_CHROME_REPO }}/releases/tag/${{ steps.release.outputs.tag }}) + branch: sync-extension-${{ steps.release.outputs.tag }} + delete-branch: true + diff --git a/sentience/browser.py b/sentience/browser.py index 3ad7c71..521a964 100644 --- a/sentience/browser.py +++ b/sentience/browser.py @@ -53,18 +53,34 @@ def __init__( def start(self) -> None: """Launch browser with extension loaded""" - # Get extension path (sentience-chrome directory) + # Try to find extension in multiple locations: + # 1. Embedded extension (sentience/extension/) - for production/CI + # 2. Development mode (../sentience-chrome/) - for local development + # __file__ is sdk-python/sentience/browser.py, so: # parent = sdk-python/sentience/ # parent.parent = sdk-python/ - # parent.parent.parent = Sentience/ (project root) - repo_root = Path(__file__).parent.parent.parent - extension_source = repo_root / "sentience-chrome" + sdk_root = Path(__file__).parent.parent + + # Check for embedded extension first (production/CI) + embedded_extension = sdk_root / "sentience" / "extension" - if not extension_source.exists(): + # Check for development extension (local development) + repo_root = sdk_root.parent + dev_extension = repo_root / "sentience-chrome" + + # Prefer embedded extension, fall back to dev extension + if embedded_extension.exists() and (embedded_extension / "manifest.json").exists(): + extension_source = embedded_extension + elif dev_extension.exists() and (dev_extension / "manifest.json").exists(): + extension_source = dev_extension + else: raise FileNotFoundError( - f"Extension not found at {extension_source}. " - "Make sure sentience-chrome directory exists." + f"Extension not found. Checked:\n" + f" 1. {embedded_extension}\n" + f" 2. {dev_extension}\n" + "Make sure extension files are available. " + "For development: cd ../sentience-chrome && ./build.sh" ) # Create temporary extension bundle diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a1d7ccf --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,59 @@ +""" +Pytest configuration and fixtures for Sentience SDK tests +""" +import os +import pytest +from pathlib import Path + + +def pytest_configure(config): + """Register custom markers""" + config.addinivalue_line( + "markers", "requires_extension: mark test as requiring the sentience-chrome extension" + ) + + +@pytest.fixture(scope="session") +def extension_available(): + """Check if the sentience-chrome extension is available""" + # Check if extension exists + # __file__ is sdk-python/tests/conftest.py + # parent = sdk-python/tests/ + # parent.parent = sdk-python/ + # parent.parent.parent = Sentience/ (project root) + repo_root = Path(__file__).parent.parent.parent + extension_source = repo_root / "sentience-chrome" + + # Also check for required extension files + if extension_source.exists(): + required_files = ["manifest.json", "content.js", "injected_api.js"] + pkg_dir = extension_source / "pkg" + if pkg_dir.exists(): + # Check if WASM files exist + wasm_files = ["sentience_core.js", "sentience_core_bg.wasm"] + all_exist = all( + (extension_source / f).exists() for f in required_files + ) and all( + (pkg_dir / f).exists() for f in wasm_files + ) + return all_exist + + return False + + +@pytest.fixture(autouse=True) +def skip_if_no_extension(request, extension_available): + """Automatically skip tests that require extension if it's not available""" + # Check if test is marked as requiring extension + marker = request.node.get_closest_marker("requires_extension") + + if marker and not extension_available: + # In CI, skip silently + # Otherwise, show a helpful message + if os.getenv("CI"): + pytest.skip("Extension not available in CI environment") + else: + pytest.skip( + "Extension not found. Build it first: cd ../sentience-chrome && ./build.sh" + ) + diff --git a/tests/test_snapshot.py b/tests/test_snapshot.py index b0678e7..febe3c5 100644 --- a/tests/test_snapshot.py +++ b/tests/test_snapshot.py @@ -7,6 +7,7 @@ from sentience.models import Snapshot +@pytest.mark.requires_extension def test_snapshot_basic(): """Test basic snapshot on example.com""" with SentienceBrowser(headless=False) as browser: @@ -23,6 +24,7 @@ def test_snapshot_basic(): for el in snap.elements) +@pytest.mark.requires_extension def test_snapshot_roundtrip(): """Test snapshot round-trip on multiple sites""" # Use sites that reliably have elements @@ -57,6 +59,7 @@ def test_snapshot_roundtrip(): # (min size 5x5, visibility, etc.) - this is acceptable +@pytest.mark.requires_extension def test_snapshot_save(): """Test snapshot save functionality""" import tempfile diff --git a/tests/test_spec_validation.py b/tests/test_spec_validation.py index f3fd032..f4cbb38 100644 --- a/tests/test_spec_validation.py +++ b/tests/test_spec_validation.py @@ -11,7 +11,10 @@ def load_schema(): """Load JSON schema from spec directory""" - repo_root = Path(__file__).parent.parent.parent + # __file__ is sdk-python/tests/test_spec_validation.py + # parent = sdk-python/tests/ + # parent.parent = sdk-python/ + repo_root = Path(__file__).parent.parent schema_path = repo_root / "spec" / "snapshot.schema.json" with open(schema_path) as f: @@ -57,6 +60,7 @@ def validate_against_schema(data: dict, schema: dict) -> list: return errors +@pytest.mark.requires_extension def test_snapshot_matches_spec(): """Test that snapshot response matches spec schema""" schema = load_schema()