Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions .github/workflows/sync-extension.yml
Original file line number Diff line number Diff line change
@@ -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

30 changes: 23 additions & 7 deletions sentience/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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"
)

3 changes: 3 additions & 0 deletions tests/test_snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion tests/test_spec_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
Loading