name: External API Live Drift # Runs the external-API LIVE-DRIFT tests against the real provider APIs so the # keyless mocks/parsers are continuously verified to still match reality. These # *.real.test.ts files self-skip in the PR lane (their LIVE gate is off) and ran # in NO workflow until now — they were dark. This lane turns the LIVE gate on and # injects the optional provider secrets. # # Provider coverage vs provider-coverage.ts (#8801): # PUBLIC (no secret, always run): Polymarket, Hyperliquid, CoinGecko. # KEY/TOKEN-GATED (self-skip via describe.skipIf unless their # _API_KEY / _ACCESS_TOKEN secret is present): Calendly, Tavily, # OpenAI, Anthropic, Strava, Oura, Withings, Fitbit. # CONFIG-BLOCKED (test exists + secret wired, but the plugin's vitest config # excludes *.real.test.ts so it can't run from here yet): Google Calendar — # see the structural-lane marker at the bottom of the steps. # A missing secret is a clean no-op (describe.skipIf), not a failure — so the # credential-blocked lanes only light up once their token secret is added to the # repo. continue-on-error keeps a provider outage / drift from red-X'ing the # branch — a failure here is a real signal that a mock has drifted from the live # API. The MTProto/native surfaces (telegram-mtproto, signal, # imessage-bluebubbles) have no HTTP drift surface and stay hand-curated # mock-only with that caveat documented in # packages/test/mocks/helpers/provider-coverage.ts. # # Nightly + on-demand; never on pull_request. on: schedule: # 09:30 UTC daily — after the other nightly live lanes settle. - cron: "30 9 * * *" workflow_dispatch: {} concurrency: group: external-api-live-drift-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: CI: "true" NODE_VERSION: "24" BUN_VERSION: "canary" NODE_OPTIONS: "--experimental-sqlite" # Turn the LIVE gate on for every external-API drift test. POLYMARKET_LIVE_TEST: "1" HYPERLIQUID_LIVE_TEST: "1" COINGECKO_LIVE_TEST: "1" CALENDLY_LIVE_TEST: "1" TAVILY_LIVE_TEST: "1" OPENAI_LIVE_TEST: "1" ANTHROPIC_LIVE_TEST: "1" STRAVA_LIVE_TEST: "1" OURA_LIVE_TEST: "1" WITHINGS_LIVE_TEST: "1" FITBIT_LIVE_TEST: "1" GOOGLE_CALENDAR_LIVE_TEST: "1" # Key/token-gated providers (tests self-skip when their secret is absent). CALENDLY_ACCESS_TOKEN: ${{ secrets.CALENDLY_ACCESS_TOKEN }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} STRAVA_ACCESS_TOKEN: ${{ secrets.STRAVA_ACCESS_TOKEN }} OURA_ACCESS_TOKEN: ${{ secrets.OURA_ACCESS_TOKEN }} WITHINGS_ACCESS_TOKEN: ${{ secrets.WITHINGS_ACCESS_TOKEN }} FITBIT_ACCESS_TOKEN: ${{ secrets.FITBIT_ACCESS_TOKEN }} GOOGLE_CALENDAR_ACCESS_TOKEN: ${{ secrets.GOOGLE_CALENDAR_ACCESS_TOKEN }} jobs: live-drift: name: External-API mocks vs live APIs (nightly) runs-on: ${{ fromJSON(vars.HETZNER_FLEET_ONLINE == 'false' && '["ubuntu-24.04"]' || '["self-hosted","hetzner-robot"]') }} timeout-minutes: 30 continue-on-error: true steps: - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 with: submodules: false show-progress: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e with: node-version: ${{ env.NODE_VERSION }} - name: Setup Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 with: bun-version: ${{ env.BUN_VERSION }} - name: Install dependencies run: bun install --frozen-lockfile - name: Polymarket live drift (public) run: bunx vitest run --root plugins/plugin-polymarket src/routes.real.test.ts - name: Hyperliquid live drift (public) run: bunx vitest run --root plugins/plugin-hyperliquid src/routes.real.test.ts - name: CoinGecko live drift (public) run: bunx vitest run --root plugins/plugin-wallet src/routes/wallet-market-overview.real.test.ts - name: Calendly live drift (token-gated, self-skips) run: bunx vitest run --root plugins/plugin-calendly src/calendly-client.real.test.ts - name: Tavily web-search live drift (key-gated, self-skips) run: bunx vitest run --root plugins/plugin-web-search src/services/webSearchService.real.test.ts - name: OpenAI live drift (key-gated, self-skips) run: bunx vitest run --root plugins/plugin-openai __tests__/openai-drift.real.test.ts - name: Anthropic live drift (key-gated, self-skips) run: bunx vitest run --root plugins/plugin-anthropic __tests__/anthropic-drift.real.test.ts - name: Strava live drift (token-gated, self-skips) run: bunx vitest run --root plugins/plugin-health test/strava-connector.real.test.ts - name: Oura live drift (token-gated, self-skips) run: bunx vitest run --root plugins/plugin-health test/oura-connector.real.test.ts - name: Withings live drift (token-gated, self-skips) run: bunx vitest run --root plugins/plugin-health test/withings-connector.real.test.ts - name: Fitbit live drift (token-gated, self-skips) run: bunx vitest run --root plugins/plugin-health test/fitbit-connector.real.test.ts # Structural-lane marker — NOT YET RUNNABLE here. # plugins/plugin-calendar/test/google-calendar-connector.real.test.ts # exists and self-gates on GOOGLE_CALENDAR_LIVE_TEST + GOOGLE_CALENDAR_ACCESS_TOKEN # (secret wired in the env block above), but plugin-calendar's vitest.config.ts # excludes `**/*.real.test.{ts,tsx}` with passWithNoTests:true, so a vitest # invocation here finds zero tests and would silently "pass" with no drift # coverage. That config exclude can only be lifted in the plugin's own config. # Wire the step below once plugin-calendar's config stops excluding real tests: # run: bunx vitest run --root plugins/plugin-calendar test/google-calendar-connector.real.test.ts