Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

496 lines
19 KiB
YAML

name: Puppeteer E2E Tests
on:
pull_request:
types: [labeled]
workflow_call: # Called by release.yml (e2e-test-gate)
secrets:
OPENROUTER_API_KEY:
required: false
SERPER_API_KEY:
required: false
workflow_dispatch: # Manual trigger runs all tests
schedule:
# Run weekly on Sunday at 2 AM UTC
- cron: '0 2 * * 0'
# No concurrency group — intentionally omitted.
# This workflow triggers on both pull_request and workflow_call (from
# ci-gate.yml / release-gate.yml). A shared concurrency key would cause
# direct PR runs and workflow_call runs to cancel each other mid-flight.
# See #3554 (reverted in #3599) for context.
permissions: {} # Minimal top-level for OSSF Scorecard
jobs:
puppeteer-tests:
name: Puppeteer E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 120
# For pull_request events, only run when labeled test:puppeteer or test:e2e.
# For all other triggers (push via workflow_call from release.yml,
# workflow_dispatch, schedule), always run.
# NOTE: workflow_call inherits the caller's github.event_name (e.g. "push"),
# so checking for 'workflow_call' never matches — use != 'pull_request' instead.
if: |
github.event_name != 'pull_request' ||
github.event.label.name == 'test:puppeteer' ||
github.event.label.name == 'test:e2e'
environment: ci
permissions:
contents: read
pull-requests: write
issues: write
env:
LABEL_NAME: ${{ github.event.label.name || 'manual' }}
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Setup PDM
uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
with:
python-version: '3.12'
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
- name: Install system dependencies
run: |
sudo apt-get update
# Note: libasound2t64 is the Ubuntu 24.04 name for libasound2
sudo apt-get install -y libsqlcipher-dev xvfb \
libnss3 libgbm1 libasound2t64 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \
fonts-liberation xdg-utils
- name: Install Python dependencies
run: pdm install
- name: Install root frontend dependencies
run: npm ci
- name: Build Vite frontend bundle
# Generates src/local_deep_research/web/static/dist/, which the
# Flask app loads via vite_helper. Without this the page renders
# without bundled CSS, causing tests to run against a partially
# unstyled UI (the responsive baseline tracked in PR #3979 was
# captured against a built bundle).
run: npm run build
- name: Install Puppeteer dependencies
working-directory: tests/puppeteer
run: |
npm ci
npx puppeteer browsers install chrome
- name: Setup directories
run: |
mkdir -p data/encrypted_databases
mkdir -p tests/puppeteer/screenshots
- name: Initialize database
env:
TEST_ENV: true
LDR_DATA_DIR: ${{ github.workspace }}/data
LDR_DB_CONFIG_KDF_ITERATIONS: "1000"
# Without LDR_TEST_MODE the 1000 is clamped to the production
# minimum (256000), making SQLCipher ~256x slower — see #4430.
LDR_TEST_MODE: "1"
run: |
cd src
pdm run python ../scripts/ci/init_test_database.py
- name: Create test configuration
run: |
# Create config directory if needed
mkdir -p ~/.config/local-deep-research
# Create settings with OpenRouter + Gemini 2.5 Flash Lite
# (Gemini 2.0 Flash is heavily rate-limited on the OpenRouter free
# pool; 2.5 Flash Lite is GA, cheaper, and far less contended.)
cat > ~/.config/local-deep-research/settings.toml << 'EOF'
[llm]
provider = "openrouter"
model = "google/gemini-2.5-flash-lite"
[search]
tool = "serper"
iterations = 1
questions_per_iteration = 2
[general]
report_type = "quick"
EOF
- name: Start LDR server
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
SERPER_API_KEY: ${{ secrets.SERPER_API_KEY }}
# Settings manager env var overrides (format: LDR_<KEY_WITH_UNDERSCORES>)
# LLM settings - use OpenRouter with Gemini 2.5 Flash Lite
LDR_LLM_PROVIDER: openrouter
LDR_LLM_MODEL: google/gemini-2.5-flash-lite
LDR_LLM_OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
# Search settings
LDR_SEARCH_TOOL: serper
LDR_SEARCH_ENGINE_WEB_SERPER_API_KEY: ${{ secrets.SERPER_API_KEY }}
# Database and server settings
LDR_DATA_DIR: ${{ github.workspace }}/data
LDR_DB_CONFIG_KDF_ITERATIONS: "1000"
# Without LDR_TEST_MODE the 1000 is clamped to the production
# minimum (256000), making SQLCipher ~256x slower — see #4430.
LDR_TEST_MODE: "1"
CI: true
LDR_DISABLE_RATE_LIMITING: true
run: |
# Start server in background
cd src
nohup pdm run python -m local_deep_research.web.app > /tmp/ldr_server.log 2>&1 &
echo "Waiting for server to start..."
for i in {1..60}; do
if curl -fsS --connect-timeout 2 --max-time 5 http://localhost:5000/api/v1/health 2>/dev/null; then
echo "Server is ready after $i seconds!"
break
fi
if ! pgrep -f 'python -m local_deep_research.web.app' > /dev/null; then
echo "Server process died!"
cat /tmp/ldr_server.log
exit 1
fi
echo "Waiting... ($i/60)"
sleep 1
done
# Final check if server is ready
if ! curl -fsS --connect-timeout 2 --max-time 5 http://localhost:5000/api/v1/health 2>/dev/null; then
echo "Server failed to start. Logs:"
cat /tmp/ldr_server.log
exit 1
fi
- name: Run Puppeteer tests
working-directory: tests/puppeteer
env:
TEST_URL: http://localhost:5000
HEADLESS: true
run: |
set -o pipefail
# Run all test suites with xvfb for virtual display
xvfb-run -a -s "-screen 0 1920x1080x24" npm run test:ci 2>&1 | tee test-output.log
TEST_RESULT="${PIPESTATUS[0]}"
# Save exit code for downstream steps
echo "TEST_EXIT_CODE=${TEST_RESULT}" >> "$GITHUB_ENV"
# Exit with test result so the step reflects pass/fail
exit "${TEST_RESULT}"
- name: Run egress-policy UI test
# Release gate for the Privacy & Egress UI (scope dropdown, the
# require-local toggles, the STRICT+meta-picker guard, and the
# data-scope propagation across pages). This test lives in
# tests/ui_tests (a standalone Puppeteer script, NOT the mocha suite
# above) and runs against the SAME booted server. It authenticates as
# the CI test user (test_admin) created by init_test_database.py and
# matched in tests/ui_tests/auth_helper.js, and skips screenshots when
# CI=true. `if: always()` so its result is reported even when the mocha
# suite above failed; the script exits non-zero on any failed check, so
# a regression fails the release gate.
if: always()
working-directory: tests/ui_tests
env:
BASE_URL: http://localhost:5000
CI: true
HEADLESS: true
run: |
set -o pipefail
npm ci
npx puppeteer browsers install chrome
xvfb-run -a -s "-screen 0 1920x1080x24" \
node test_egress_policy_ui.js 2>&1 | tee egress-ui-output.log
- name: Extract test summary
if: always()
working-directory: tests/puppeteer
run: |
# Extract pass/fail summary from mocha output
if [ -f test-output.log ]; then
# Get the summary line (e.g., "26 passing (3m)")
PASSING=$(grep -oP '\d+ passing' test-output.log | tail -1 || echo "0 passing")
FAILING=$(grep -oP '\d+ failing' test-output.log | tail -1 || echo "0 failing")
echo "TESTS_PASSING=${PASSING}" >> "$GITHUB_ENV"
echo "TESTS_FAILING=${FAILING}" >> "$GITHUB_ENV"
# Extract key test results (collection creation, subscription, research)
echo "## Key Test Results" > test-summary.md
echo "" >> test-summary.md
# Check for collection creation
if grep -q "Collection found in dropdown: true" test-output.log; then
echo "- ✅ Collection creation: **Success**" >> test-summary.md
elif grep -q "Collection found in dropdown: false" test-output.log; then
echo "- ❌ Collection creation: **Failed**" >> test-summary.md
fi
# Check for subscription creation
if grep -q "Subscription name found: true" test-output.log; then
echo "- ✅ Subscription creation: **Success**" >> test-summary.md
elif grep -q "Subscription name found: false" test-output.log; then
echo "- ❌ Subscription creation: **Failed**" >> test-summary.md
fi
# Check for research completion
if grep -q "Research completed: true" test-output.log; then
RESEARCH_TIME=$(grep -oP "Research completed: true \(took \K\d+(?=s\))" test-output.log || echo "?")
echo "- ✅ Research workflow: **Completed** (${RESEARCH_TIME}s)" >> test-summary.md
elif grep -q "Research completed: false" test-output.log; then
echo "- ⚠️ Research workflow: **Timed out**" >> test-summary.md
fi
# Check for settings persistence
if grep -q "After reload value:" test-output.log; then
echo "- ✅ Settings persistence: **Working**" >> test-summary.md
fi
# Add screenshot count
SCREENSHOT_COUNT=$(find screenshots -name "*.png" 2>/dev/null | wc -l || echo "0")
echo "" >> test-summary.md
echo "**Screenshots captured:** ${SCREENSHOT_COUNT}" >> test-summary.md
fi
- name: Show test summary in workflow UI
if: always()
working-directory: tests/puppeteer
# zizmor: ignore[template-injection] — env.TEST_EXIT_CODE/TESTS_PASSING/TESTS_FAILING are set by this workflow, not user input
run: |
# Output to GITHUB_STEP_SUMMARY so results are visible in Actions UI
{
echo "## 🧪 Puppeteer E2E Test Results"
echo ""
if [ "$TEST_EXIT_CODE" == "0" ]; then
echo "### ✅ Tests Passed!"
else
echo "### ❌ Tests Failed"
fi
echo ""
echo "**Summary:** $TESTS_PASSING, $TESTS_FAILING"
echo ""
# Include key test results
if [ -f test-summary.md ]; then
cat test-summary.md
echo ""
fi
echo "---"
echo ""
echo "<details>"
echo "<summary>📋 Last 50 lines of test output</summary>"
echo ""
echo '```'
if [ -f test-output.log ]; then
tail -50 test-output.log
fi
echo '```'
echo "</details>"
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload server logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: server-logs
path: /tmp/ldr_server.log
retention-days: 7
- name: Upload test artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: puppeteer-test-results
path: |
tests/puppeteer/test-output.log
tests/puppeteer/test-summary.md
tests/puppeteer/screenshots/
retention-days: 7
- name: Find PR number
id: find-pr
if: always()
# zizmor: ignore[template-injection] — github.ref_name and github.repository are safe GitHub context values
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EVENT_NAME: ${{ github.event_name }}
PR_NUM: ${{ github.event.pull_request.number }}
REF_NAME: ${{ github.ref_name }}
REPO: ${{ github.repository }}
run: |
if [ "$EVENT_NAME" == "pull_request" ]; then
# Labeled PR run — the primary, correct path. Real branches targeting main.
echo "pr_number=$PR_NUM" >> "$GITHUB_OUTPUT"
elif [ "$EVENT_NAME" == "workflow_dispatch" ] && [ "$REF_NAME" != "main" ]; then
# Manual run on a feature branch. Map branch -> its open PR, but only a
# UNIQUE, SAME-REPO match: cross-repo (fork) PRs and ambiguous matches are
# skipped so we never post to the wrong PR (e.g. a fork PR'd from `main`).
PR_NUMBER=$(gh pr list --head "$REF_NAME" --state open --repo "$REPO" \
--json number,isCrossRepository \
--jq '[.[] | select(.isCrossRepository == false)]
| if length == 1 then .[0].number | tostring else "" end')
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
else
# release.yml workflow_call (event=push) and the weekly schedule both run
# on the default branch. There is never a PR from main, so any
# `gh pr list --head main` match is a coincidental fork PR — skip
# entirely (results still go to the Actions step summary). See #3884.
echo "pr_number=" >> "$GITHUB_OUTPUT"
echo "event=$EVENT_NAME ref=$REF_NAME has no associated PR — skipping comment"
fi
- name: Build PR comment
if: always() && steps.find-pr.outputs.pr_number != ''
# zizmor: ignore[template-injection] — env.TEST_EXIT_CODE/TESTS_PASSING/TESTS_FAILING are set by this workflow, not user input
env:
EVENT_NAME: ${{ github.event_name }}
run: |
{
# Hidden marker — the "Post or update PR comment" step finds this
# comment by this marker so re-runs edit it in place (see #3884).
echo '<!-- puppeteer-e2e-results -->'
echo "## 🧪 Puppeteer E2E Test Results"
echo ""
if [ "$TEST_EXIT_CODE" == "0" ]; then
echo "### ✅ Tests Passed!"
else
echo "### ❌ Tests Failed"
fi
echo ""
echo "**Summary:** $TESTS_PASSING, $TESTS_FAILING"
echo ""
# Include key test results
if [ -f tests/puppeteer/test-summary.md ]; then
cat tests/puppeteer/test-summary.md
echo ""
fi
# Include research output if available
if [ -f tests/puppeteer/research-output/research-result.txt ]; then
echo "---"
echo ""
echo "### 📝 Research Output"
echo ""
if [ -f tests/puppeteer/research-output/research-metadata.json ]; then
QUERY=$(jq -r '.query // "unknown"' tests/puppeteer/research-output/research-metadata.json)
echo "**Query:** \`$QUERY\`"
echo ""
fi
echo "<details>"
echo "<summary>📄 Research Result (click to expand)</summary>"
echo ""
echo '```markdown'
# Get first 5000 chars of research output
head -c 5000 tests/puppeteer/research-output/research-result.txt
TOTAL_SIZE=$(wc -c < tests/puppeteer/research-output/research-result.txt)
if [ "$TOTAL_SIZE" -gt 5000 ]; then
echo ""
echo "... [truncated, total $TOTAL_SIZE chars]"
fi
echo '```'
echo "</details>"
echo ""
fi
echo "---"
echo ""
echo "<details>"
echo "<summary>📋 Full Test Output (click to expand)</summary>"
echo ""
echo '```'
# Get last 100 lines of test output (most relevant)
if [ -f tests/puppeteer/test-output.log ]; then
tail -100 tests/puppeteer/test-output.log
fi
echo '```'
echo "</details>"
echo ""
echo "---"
echo ""
echo "**Configuration:**"
echo "- Model: \`google/gemini-2.5-flash-lite\` (via OpenRouter)"
echo "- Search: \`serper\`"
echo "- Test suite: \`all\`"
echo ""
echo "_Triggered by: \`${EVENT_NAME}\` (label: \`${LABEL_NAME}\`)_"
} > comment.md
- name: Post or update PR comment
if: always() && steps.find-pr.outputs.pr_number != ''
continue-on-error: true # required release gate must not fail on a GitHub API hiccup
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
with:
retries: 3
script: |
const fs = require('fs');
const issue_number = Number(process.env.PR_NUMBER);
const marker = '<!-- puppeteer-e2e-results -->';
const body = fs.readFileSync('comment.md', 'utf8');
// Paginate — on a long-lived PR the existing comment may be past the
// first page (default 30), which would otherwise create a new comment
// every run and reintroduce the spam this fix removes (#3884).
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
per_page: 100,
});
const existing = comments.find(
(c) => c.user.type === 'Bot' && c.body.includes(marker)
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number,
body,
});
}
- name: Remove label for re-triggering
if: always() && github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUM: ${{ github.event.pull_request.number }}
run: |
gh issue edit "$PR_NUM" --remove-label "$LABEL_NAME" 2>/dev/null || true
- name: Cleanup
if: always()
run: |
# Kill server
pkill -f 'python -m local_deep_research.web.app' || true
rm -f comment.md