Integrations Smoke Test Runbook
Manual end-to-end test for OAuth + ingestion + proposal generation per provider. Run against Vercel Preview before promoting env vars to Production.
Status: HubSpot (Phase 2) and Google Drive (Phase 4) sections complete. QuickBooks / Gmail sections will be added as their phases land.
HubSpot
1. Prerequisites
- HubSpot developer portal account with an app configured at https://developers.hubspot.com.
- Redirect URI in the HubSpot app set to
${NEXT_PUBLIC_APP_URL}/api/integrations/hubspot/callbackfor the environment under test (Preview first). - App scopes configured (must match
lib/integrations/providers.ts):crm.objects.contacts.readcrm.objects.companies.readcrm.objects.deals.read
- Vercel env vars (Preview, then Production after smoke pass):
HUBSPOT_CLIENT_IDHUBSPOT_CLIENT_SECRETINTEGRATIONS_HUBSPOT_ENABLED=true
- Fly.io worker secrets (
fly secrets list -a boots-proposal-worker):HUBSPOT_CLIENT_IDHUBSPOT_CLIENT_SECRETBOOTS_BRAIN_URLBRAIN_API_SECRETINTEGRATIONS_USE_FIXTURESis unset orfalse(default).
- Worker deployed and healthy:
fly status -a boots-proposal-workerandfly logs -a boots-proposal-worker.
2. OAuth connect
- Sign in to the app under test as an org owner or admin (Connect requires
admin/ownerper PR #223). - Navigate to
/settings/integrations. - Click Connect on the HubSpot card.
- Authorize in the HubSpot consent screen.
- Confirm redirect lands on
/settings/integrationswith a success state and the card now shows a Disconnect button.
Verification SQL (run in Supabase SQL editor against the env under test):
SELECT id, provider, status, last_synced_at, created_at
FROM connected_sources
WHERE org_id = '<your-org-id>'
AND provider = 'hubspot'
AND deleted_at IS NULL;
-- Expected: one row, status = 'active' (or 'pending' until first sync completes)
3. First sync
The OAuth callback enqueues a full_sync job on pgmq. The Fly.io worker should pick it up within INGESTION_POLL_INTERVAL_MS (default 5s).
Tail the worker:
fly logs -a boots-proposal-worker
Expected log lines: Token needs refresh (only if access token already expired), then job state transitions to running → completed.
Verification SQL:
SELECT id, status, items_total, items_succeeded, items_failed, error_message, completed_at
FROM ingestion_jobs
WHERE org_id = '<your-org-id>'
ORDER BY created_at DESC
LIMIT 3;
-- Expected: status = 'completed', items_failed = 0
SELECT document_type, count(*)
FROM source_documents
WHERE org_id = '<your-org-id>'
GROUP BY document_type;
-- Expected: rows for hubspot.contact, hubspot.company, hubspot.deal
SELECT client_name, source_id, created_at
FROM client_intelligence
WHERE org_id = '<your-org-id>'
AND source_id IS NOT NULL
LIMIT 10;
-- Expected: one row per HubSpot company with source_id populated
4. Proposal generation
Create a new proposal in the app for a client whose name matches a HubSpot company name from step 3. After generation completes, query attribution:
SELECT sa.agent_role, sa.section_name, ci.client_name, cs.provider
FROM source_attribution sa
JOIN client_intelligence ci ON ci.id = sa.client_intelligence_id
JOIN connected_sources cs ON cs.id = ci.source_id
WHERE sa.proposal_id = '<proposal-id>';
-- Expected (after Step 9 / Phase 3 ships): rows showing HubSpot data was attributed
-- Note: pre-Step-9, this query returns 0 rows even on a successful proposal
5. Token refresh
- Note the current
token_expires_atfor the HubSpot row inconnected_sources. - Manually backdate it via Supabase SQL:
UPDATE connected_sources SET token_expires_at = '2000-01-01T00:00:00Z' WHERE org_id = '<your-org-id>' AND provider = 'hubspot'; - Trigger a manual refresh (re-enqueue from the UI or insert a pgmq message manually).
- Watch worker logs for
Token needs refreshfollowed by a successful sync. - Verify new
token_expires_atis in the future and no Sentry errors fired.
6. Error path — revoked token
- In HubSpot developer portal, revoke the connected app's access for your test account (User → Connected Apps → Disconnect).
- Trigger a manual sync.
- Expected:
connected_sources.status = 'error'status_messagecontainsrefresh_token_invalid(seeworker/src/integrations/token-refresh.ts:108-116)./settings/integrationsshows an error badge and a Disconnect button so the user can reconnect.
7. Failure modes — quick reference
| Symptom in UI | Root cause | Fix |
|---|---|---|
| HubSpot card shows "Coming soon" | INTEGRATIONS_HUBSPOT_ENABLED not 'true' in this env |
Set the env var in Vercel for the matching environment |
| Connect button greyed with "OAuth not configured" | HUBSPOT_CLIENT_ID or HUBSPOT_CLIENT_SECRET missing in Vercel |
Set both in Vercel env, redeploy |
Connect succeeds, status stays pending |
Worker hasn't picked up the job, or callback didn't enqueue | fly logs -a boots-proposal-worker; check ingestion_jobs for the row |
ingestion_jobs.status='failed' with 403 in error_message |
Missing crm.objects.companies.read scope on HubSpot app |
Add scope in HubSpot app, disconnect, reconnect |
ingestion_jobs.status='failed' with brain URL error |
Worker missing BOOTS_BRAIN_URL / BRAIN_API_SECRET |
fly secrets set both, restart worker |
source_documents empty after sync |
Real client returned 0 results, or wrong scopes | Confirm test HubSpot account actually has contacts/companies/deals |
client_intelligence empty but documents exist |
Companies have no name property on payload |
Check raw payloads in source_documents.payload |
connected_sources.status='error' after refresh |
refresh_token rejected (user revoked or HubSpot rotated) |
User must Disconnect → Connect again |
8. After smoke passes
Promote env vars to Production:
INTEGRATIONS_HUBSPOT_ENABLED=true
HUBSPOT_CLIENT_ID=…
HUBSPOT_CLIENT_SECRET=…
Add the production redirect URI to the HubSpot app. Repeat steps 2–6 against Production with a real customer account once.
Google Drive
1. Prerequisites
- Google Cloud project with an OAuth 2.0 Web client at https://console.cloud.google.com/apis/credentials.
- Authorized redirect URI on the OAuth client set to
${NEXT_PUBLIC_APP_URL}/api/integrations/google_drive/callbackfor the environment under test (Preview first). - OAuth consent screen scopes (must match
lib/integrations/providers.ts):https://www.googleapis.com/auth/drive.readonly
- Drive API enabled in the same Google Cloud project (
APIs & Services → Library → Google Drive API → Enable). - Vercel env vars (Preview, then Production after smoke pass):
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETINTEGRATIONS_GOOGLE_DRIVE_ENABLED=true
- Fly.io worker secrets (
fly secrets list -a boots-proposal-worker):GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETBOOTS_BRAIN_URLBRAIN_API_SECRETINTEGRATIONS_USE_FIXTURESis unset orfalse(default).- Optional:
DRIVE_INITIAL_SYNC_MAX_FILES(default 200).
- Worker deployed and healthy:
fly status -a boots-proposal-worker. - Test Google account has at least a few Google Docs and Sheets in its Drive (other file types are filtered out by the client).
2. OAuth connect
- Sign in to the app under test as an org owner or admin (Connect requires
admin/owner). - Navigate to
/settings/integrations. - Click Connect on the Google Drive card.
- Authorize in the Google consent screen — make sure the Drive read-only scope is granted.
- Confirm redirect lands on
/settings/integrationswith the card now showing a Disconnect button.
Verification SQL:
SELECT id, provider, status, last_synced_at, created_at
FROM connected_sources
WHERE org_id = '<your-org-id>'
AND provider = 'google_drive'
AND deleted_at IS NULL;
-- Expected: one row, status = 'active' (or 'pending' until first sync completes)
3. First sync
The OAuth callback enqueues a full_sync job on pgmq. The Fly.io worker picks it up within INGESTION_POLL_INTERVAL_MS (default 5s). Initial sync is capped at DRIVE_INITIAL_SYNC_MAX_FILES (default 200) Docs/Sheets.
fly logs -a boots-proposal-worker
Verification SQL:
SELECT id, status, items_total, items_succeeded, items_failed, error_message, completed_at
FROM ingestion_jobs
WHERE org_id = '<your-org-id>'
ORDER BY created_at DESC
LIMIT 3;
-- Expected: status = 'completed', items_failed = 0
SELECT document_type, count(*)
FROM source_documents
WHERE org_id = '<your-org-id>'
GROUP BY document_type;
-- Expected: rows for google_drive.doc and/or google_drive.sheet
SELECT external_id, raw_payload->>'name' AS name,
raw_payload->>'mimeType' AS mime,
length(raw_payload->>'exported_text') AS exported_chars
FROM source_documents
WHERE org_id = '<your-org-id>'
AND document_type LIKE 'google_drive.%'
ORDER BY exported_chars DESC
LIMIT 10;
-- Expected: file names match what's in your Drive,
-- exported_chars > 0 for non-empty Docs/Sheets.
Note: Google Drive does not produce
client_intelligencerollups (seeworker/src/handlers/ingestion.ts—buildRollupsreturns[]forgoogle_drive). Drive content reaches the strategist viasource_documentssemantic search, not rollups.
4. Proposal generation
Drive documents are surfaced to the strategist through source_documents semantic search rather than client_intelligence rollups. To verify Drive content can influence a proposal:
- Ensure the test Drive contains a Doc whose body mentions a known client name (e.g., the client you'll generate for).
- Generate a proposal for that client.
- Inspect the strategist context (or check Sentry / worker logs) to confirm the Doc was retrieved.
Source attribution for vector-search hits is not wired yet — see the match_client_intelligence follow-up in docs/plans/PRD-integrations-steps8to10.md.
5. Token refresh
- Note the current
token_expires_atfor the Drive row inconnected_sources. - Backdate it via Supabase SQL:
UPDATE connected_sources SET token_expires_at = '2000-01-01T00:00:00Z' WHERE org_id = '<your-org-id>' AND provider = 'google_drive'; - Trigger a manual refresh (re-enqueue from the UI or insert a pgmq message manually).
- Watch worker logs for
Token needs refreshfollowed by a successful sync. - Verify new
token_expires_atis in the future and no Sentry errors fired.
Google refresh tokens require
access_type=offlineandprompt=consenton the auth request — these are already set inPROVIDERS.google_drive.extraAuthParams(seelib/integrations/providers.ts). Ifconnected_sources.refresh_token_vault_idis null after the first connect, the consent screen ran withoutprompt=consent— disconnect and reconnect.
6. Error path — revoked token
- Revoke the connected app's access at https://myaccount.google.com/permissions (find the test app and remove access).
- Trigger a manual sync.
- Expected:
connected_sources.status = 'error'status_messagecontainsrefresh_token_invalid(seeworker/src/integrations/token-refresh.ts)./settings/integrationsshows an error badge and a Disconnect button so the user can reconnect.
7. Failure modes — quick reference
| Symptom in UI | Root cause | Fix |
|---|---|---|
| Drive card shows "Coming soon" | INTEGRATIONS_GOOGLE_DRIVE_ENABLED not 'true' in this env |
Set the env var in Vercel for the matching environment |
| Connect button greyed with "OAuth not configured" | GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET missing in Vercel |
Set both in Vercel env, redeploy |
Connect succeeds, status stays pending |
Worker hasn't picked up the job, or callback didn't enqueue | fly logs -a boots-proposal-worker; check ingestion_jobs for the row |
ingestion_jobs.status='failed' with 403 in error_message |
Drive API not enabled in Google Cloud project, or scope missing | Enable Drive API; verify drive.readonly granted; reconnect |
ingestion_jobs.status='failed' with 429 after retries |
Drive rate limit exceeded; sync cap likely needs lowering | Lower DRIVE_INITIAL_SYNC_MAX_FILES, retry the job |
ingestion_jobs completes but items_total=0 |
No Google Docs or Sheets in the test account (binary files are filtered) | Create a few Docs/Sheets and trigger a manual sync |
source_documents.exported_text is empty |
Drive export endpoint returned empty body — likely a Doc with no content |
Open the Doc in Drive and confirm it has body text |
connected_sources.status='error' after refresh |
Refresh token rejected (user revoked or Google rotated) | User must Disconnect → Connect again with prompt=consent |
8. After smoke passes
Promote env vars to Production:
INTEGRATIONS_GOOGLE_DRIVE_ENABLED=true
GOOGLE_CLIENT_ID=…
GOOGLE_CLIENT_SECRET=…
Add the production redirect URI to the Google Cloud OAuth client. Repeat steps 2–6 against Production with a real customer account once.
QuickBooks
Not yet implemented — see Phase 5.
Gmail
Not yet implemented — see Phase 6.
Ready to create AI-powered proposals?
Start Free