Skip to content

fix: sync docstore drive selections#972

Merged
maxtechera merged 2 commits into
stagingfrom
fix/support-19-drive-picker
Feb 20, 2026
Merged

fix: sync docstore drive selections#972
maxtechera merged 2 commits into
stagingfrom
fix/support-19-drive-picker

Conversation

@maxtechera
Copy link
Copy Markdown
Collaborator

@maxtechera maxtechera commented Feb 19, 2026

Summary

  • Propagate Google Drive/Gmail selections to docstore preview/process payloads.
  • Preserve saved Google Drive selections unless the credential changes.
  • Normalize Google Drive selectedFiles to accept id arrays and picker object arrays.

Why

  • Docstore Preview/Process was using stale loader state, so selected files never saved or sent.
  • Loader expected string ids and failed on picker object arrays, causing Drive 404s.

Validation

  • Not run (per request)

@linear
Copy link
Copy Markdown

linear Bot commented Feb 19, 2026

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
answerai-docs Ready Ready Preview Feb 19, 2026 11:26pm
the-answerai Ready Ready Preview Feb 19, 2026 11:26pm

Request Review

@claude
Copy link
Copy Markdown

claude Bot commented Feb 19, 2026

PR Review: Fix/support-19 Drive Picker

Summary: This PR fixes Google Drive token refresh logic in GoogleDrivePicker.jsx by adding a fallback to the credentialsApi.refreshAccessToken path when the oauth2Api.refresh call indicates missing OAuth configuration. It also adds opencode.json for OpenCode IDE integration and updates AGENTS.md with a comprehensive index of commands, agents, skills, and rules.

The fix addresses a real user-facing bug (SUPPORT-19) where re-authentication fails due to a missing OAuth config scenario. The changes are minimal and targeted, which is appropriate for a support fix.


Critical Issues

None identified. No security regressions, no multi-tenancy bypasses, and no new unprotected routes were introduced.


Major Concerns

1. Duplicated fallback logic - GoogleDrivePicker.jsx lines 314-334

The same credentialsApi.refreshAccessToken fallback is called in two separate places: once inside the if (oauth2Failed && isMissingOauthConfig) branch, and again inside the catch block. This creates two code paths that do the same thing and makes the intent harder to follow.

The original failure mode is:

  • A) oauth2Api.refresh returns HTTP 2xx with { success: false, message: "Missing required OAuth configuration..." } — handled by the if branch.
  • B) oauth2Api.refresh throws an HTTP error (4xx/5xx) and the error message contains "Missing required OAuth configuration" — handled by the catch block.

However, looking at the server implementation (packages/server/src/routes/oauth2/index.ts line 330-333), the endpoint returns HTTP 400 with { success: false, message: "Missing required OAuth configuration..." }. This means the Axios client will throw an error (not return a resolved response with success: false), so Path A will never actually be hit in practice — the catch block is the real execution path.

Consider simplifying to a single catch-only approach, or verifying whether the server ever returns HTTP 200 with success: false for this case. Having dead code in an error-handling path is risky because it creates a false sense of coverage.

// Suggested simplified version:
const handleRefreshAccessToken = useCallback(async () => {
    if (!credentialId) return
    try {
        setIsRefreshing(true)
        try {
            await oauth2Api.refresh(credentialId)
        } catch (error) {
            const errorMessage = error.response?.data?.message || error.message || ''
            if (errorMessage.includes('Missing required OAuth configuration')) {
                await credentialsApi.refreshAccessToken({ credentialId })
            } else {
                throw error
            }
        }
        getCredentialDataApi.request(credentialId)
        setIsTokenExpired(false)
        setIsReauthRequired(false)
        showSnackbar('Successfully refreshed access token', 'success')
    } catch (error) {
        // ... existing error handling
    } finally {
        setIsRefreshing(false)
    }
}, [credentialId, getCredentialDataApi, showSnackbar])

2. Silent string-matching for control flow - GoogleDrivePicker.jsx lines 318, 327

The fallback decision is driven by string matching on error messages ('Missing required OAuth configuration'). If the server-side error message ever changes (typo fix, i18n, refactor), the fallback silently stops working with no test coverage to catch it. Consider:

  • Defining a shared error code constant (e.g., MISSING_OAUTH_CONFIG) that both the server response and client check against, rather than matching on a free-form string.
  • Or at minimum, extract the string to a named constant so it fails loudly if changed.

Minor Issues and Suggestions

3. opencode.json - consider adding to .gitignore

opencode.json is an IDE/tooling configuration file similar to .vscode/settings.json. Whether this belongs in the repo depends on team policy. If the team uses multiple IDEs, committing one IDE's config file can cause friction. If the intent is to standardise the OpenCode setup for all contributors, a brief note in CLAUDE.md or README.md pointing to this file would help discoverability.

4. Missing await risk on getCredentialDataApi.request

getCredentialDataApi.request(credentialId) at line 336 (post-refresh) is called without await. This is consistent with the pre-existing code pattern, so it is not a regression introduced by this PR. However, if the request fails, the error will be silently swallowed. This is worth noting for a future cleanup pass.

5. No tests for the new fallback path

The new branching logic in handleRefreshAccessToken is not covered by any tests visible in the diff. Given that this fix targets a specific production failure scenario (SUPPORT-19), a unit test asserting that credentialsApi.refreshAccessToken is called when oauth2Api.refresh returns/throws with the "Missing required OAuth configuration" message would significantly reduce regression risk.


Positive Observations

  • The fix is appropriately scoped — only the minimum necessary change was made to GoogleDrivePicker.jsx.
  • The existing outer try/catch structure (lines 312-352) is preserved correctly, so the new inner try/catch integrates cleanly.
  • The AGENTS.md index is a useful addition for onboarding and navigation. The table format is clear and easy to scan.
  • The opencode.json MCP integrations (Linear, Jira, GitHub, Render) are well-structured and reflect the actual tools used by the team.
  • Re-authentication UX (isReauthRequired, snackbar messages) remains intact and untouched, which is correct for a targeted fix.

TheAnswer-Specific Checklist

  • No new routes added (no enforceAbility check required)
  • No new database queries (no organizationId filter required)
  • No new Flowise components (no tags: ['AAI'] required)
  • No new migrations required
  • Error messages are user-friendly and do not leak internal details
  • Test coverage for the new fallback path (missing)

Next Steps

  1. For the PR author: Consider simplifying the double-fallback pattern (Major Concern Answers Integration Beta v1 #1) to remove the unreachable if (oauth2Failed && isMissingOauthConfig) branch, or confirm with a server-side test that the 200+success:false path can actually be triggered.
  2. Follow-up ticket: Add a shared constant for the 'Missing required OAuth configuration' string to avoid silent breakage from message changes.
  3. Follow-up ticket: Add a unit test for the handleRefreshAccessToken fallback path.

Overall this is a reasonable targeted fix for a production support issue. The major concern is about code clarity and a potentially unreachable branch, not correctness — the fallback via the catch block will work for the reported failure scenario.


Review generated by Claude Code PR Review Agent

@maxtechera
Copy link
Copy Markdown
Collaborator Author

Scoped this PR to the intended fix only. Removed unrelated AGENTS/opencode changes so the diff now only contains GoogleDrivePicker refresh fallback logic.

@claude
Copy link
Copy Markdown

claude Bot commented Feb 19, 2026

PR Review: Fix/support 19 Drive Picker

Summary: This PR adds a fallback mechanism in the GoogleDrivePicker component for cases where the primary oauth2Api.refresh endpoint fails due to a missing OAuth configuration (e.g., when the credential was set up without clientId/clientSecret/accessTokenUrl). In that specific case, the code falls back to the TheAnswer-specific credentialsApi.refreshAccessToken endpoint. The fix is small (22 additions, 1 deletion) and targeted, but the implementation has some correctness and maintainability concerns worth addressing before merge.


Critical Issues

1. Silent swallowing of unexpected errors inside the inner try/catch

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx lines 325-333

The inner catch block checks isMissingOauthConfig and re-throws if false, but when oauth2Api.refresh itself throws an HTTP error (e.g., network failure, 500), the error.message will be the raw Axios error message, not the server-side message. The fallback check errorMessage.includes('Missing required OAuth configuration') will correctly not match and the error will be re-thrown — that part is fine. The concern is the code path before the catch: when oauth2Failed && !isMissingOauthConfig, a new Error(oauth2Message) is thrown and caught by the inner catch, then also re-thrown. This double-throw path works but creates confusing double-wrapping — the error is created and then immediately caught by the same inner block. Consider extracting this logic more clearly.

// Current flow (inner catch re-throws an error it just created):
} else if (oauth2Failed) {
    throw new Error(oauth2Message || 'Error refreshing access token')
    // ^^ caught by the inner catch immediately below
}
} catch (error) {
    // ... re-throws the above error
    if (!isMissingOauthConfig) { throw error }
}

The outer catch on line 340 will still handle it, but the two-layer try/catch with interplay between the blocks is harder to reason about. Suggest merging the success-response error path (oauth2Failed && !isMissingOauthConfig) out of the inner try block.


2. The oauth2-credential routes lack enforceAbility middleware (pre-existing, newly exercised)

Location: packages/server/src/routes/oauth2/index.ts lines 72, 157, 307

This is a pre-existing issue that this PR now makes more prominent. The oauth2Api.refresh call targets POST /api/v1/oauth2-credential/refresh/:credentialId. That route has no enforceAbility or checkPermission middleware and performs no organizationId scoping:

// Line 314 — no organizationId filter, no auth check
const credential = await credentialRepository.findOneBy({ id: credentialId })

Any authenticated user who knows a credentialId (a UUID) from another organization can trigger a token refresh against that credential. This is a multi-tenancy violation and a security concern. While this PR does not introduce this issue, the fallback path it adds relies on this unprotected route as the primary attempt. This should be filed as a separate ticket if not already tracked.


Major Concerns

3. Inconsistent response shape detection is fragile

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx lines 316-318

const oauth2Message = response?.data?.message || ''
const oauth2Failed = response?.data?.success === false
const isMissingOauthConfig = oauth2Message.includes('Missing required OAuth configuration')

The server returns { success: false, message: 'Missing required OAuth configuration: clientId, clientSecret, or refresh_token' } at line 330 of oauth2/index.ts. The string comparison 'Missing required OAuth configuration' is a substring match against a server-defined message. This coupling is fragile — if the server message ever changes (e.g. during a Flowise upstream merge), this fallback silently breaks without any test coverage to catch it.

Consider: defining a machine-readable error code (e.g. errorCode: 'MISSING_OAUTH_CONFIG') on the server response and checking for that on the client instead of string matching.

4. Success path does not await the fallback call result

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx lines 321, 333

Both fallback calls to credentialsApi.refreshAccessToken are awaited, which is correct. However there is no check on the result of this fallback call. If credentialsApi.refreshAccessToken fails, the error will propagate to the outer catch, which is correct behavior — but there is also no success validation. Consider verifying the fallback response is successful before proceeding.

5. The fallback call also duplicates logic across two branches

Location: Lines 320-333

The isMissingOauthConfig fallback appears in both the success path (lines 320-321) and the catch path (lines 326-333). This means the same fallback is attempted in two places, making it unclear which path is the actual primary fallback. If oauth2Api.refresh returns { success: false, message: '...' } with an HTTP 200, the inner try success path handles it. If the server throws an HTTP error status, the catch path handles it. This is a valid pattern given the server's inconsistent use of HTTP status codes (some errors return 400 while others return 200 with success: false), but it should be documented with a comment.


Minor Issues and Suggestions

6. Missing await result validation for getCredentialDataApi.request

Location: Line 336

getCredentialDataApi.request(credentialId)

This is a fire-and-forget call that was present before this PR and continues unchanged. It is not awaited. This is likely intentional (the hook handles state updates asynchronously), but worth documenting.

7. Error message extraction in catch could surface confusing messages

Location: Lines 326-327

const errorMessage = error.response?.data?.message || error.message || ''

If oauth2Api.refresh fails with a 4xx/5xx that has a structured error body (as the server does for some cases), error.response?.data?.message could be undefined if the server returns a non-standard shape. The existing fallback chain handles this adequately, but a test covering a failed HTTP response would make this safer.

8. No test coverage for the new fallback logic

The fallback mechanism involves multiple conditional branches. There are no unit tests added for this component. Given the fragility of the string-matching approach, tests covering:

  • oauth2Api.refresh succeeds with success: false + missing config message
  • oauth2Api.refresh throws with a missing config message in the HTTP error
  • oauth2Api.refresh throws with an unrelated error (should not call fallback)

...would significantly reduce the risk of regressions here.


Positive Observations

  • The dual-path approach (checking both the success response body and the catch block) correctly handles the server's inconsistent error signaling (HTTP 200 with success: false vs. thrown HTTP errors).
  • The outer try/catch structure on handleRefreshAccessToken correctly surfaces re-auth-required errors (REAUTH_REQUIRED, 401) and generic errors separately, which is good UX.
  • Keeping the fallback scoped only to the 'Missing required OAuth configuration' case avoids masking other legitimate errors.
  • The PR is well-scoped — it only touches the specific refresh logic and does not introduce unrelated changes (the earlier docs commits were removed in the final commit 5d5274e).
  • The finally block on line 350 correctly always calls setIsRefreshing(false), preventing the UI from getting stuck in a loading state.

Next Steps

  1. Address the fragile string-matching by introducing a structured error code on the server response (errorCode: 'MISSING_OAUTH_CONFIG') — this is the highest-leverage improvement.
  2. Simplify the double-try/catch by consolidating both isMissingOauthConfig branches.
  3. Add a separate ticket to add enforceAbility + organizationId scoping to the oauth2-credential routes if not already tracked.
  4. Consider adding unit tests for the three fallback branches described above.

Overall: the fix solves a real user-facing issue (Drive Picker broken when OAuth config is incomplete) but the implementation would benefit from the refactoring above before merging.

@maxtechera maxtechera force-pushed the fix/support-19-drive-picker branch from 5d5274e to 23cd1cd Compare February 19, 2026 23:34
@maxtechera maxtechera changed the title Fix/support 19 drive picker fix: add Google Drive OAuth refresh fallback Feb 19, 2026
@claude
Copy link
Copy Markdown

claude Bot commented Feb 19, 2026

PR Review: fix: add Google Drive OAuth refresh fallback

Reviewer: Claude Code (automated review)
Scope: 1 file changed, +22 / -1 lines
File: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx


Summary

This PR adds a fallback path inside handleRefreshAccessToken so that when oauth2Api.refresh() reports a missing OAuth configuration, the component retries using credentialsApi.refreshAccessToken() instead. The intent is correct and solves a real environment-compatibility problem. The overall structure is reasonable, but there are a few issues worth addressing before merge.


Critical Issues

1. Silent double-invocation of credentialsApi.refreshAccessToken when oauth2Api.refresh throws a network/HTTP error that also happens to mention "Missing required OAuth configuration"

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx:325-333

The outer catch block intercepts all errors thrown by oauth2Api.refresh, including genuine HTTP 4xx/5xx responses. If the server returns a 400 or 500 response whose body contains the substring "Missing required OAuth configuration", the code treats that as a "missing config" signal and silently falls back to credentialsApi.refreshAccessToken. This could mask a real server-side error (e.g. a misconfigured credential that should prompt the user to fix the credential, not just try a different refresh path).

The inner try/catch already handles the case where oauth2Api.refresh resolves successfully but returns success: false. The outer catch should therefore only catch errors thrown by the credentialsApi.refreshAccessToken call in the inner block (or legitimate network failures), not re-intercept errors from oauth2Api.refresh.

// Current: outer catch re-intercepts oauth2Api.refresh errors
} catch (error) {
    const errorMessage = error.response?.data?.message || error.message || ''
    const isMissingOauthConfig = errorMessage.includes('Missing required OAuth configuration')
    if (!isMissingOauthConfig) {
        throw error
    }
    await credentialsApi.refreshAccessToken({ credentialId })
}

The intent was to catch the case where oauth2Api.refresh throws (rather than returns a failure response). But the same string-matching logic is duplicated in both the inner and outer blocks, and the outer block will also catch errors thrown by credentialsApi.refreshAccessToken in the inner block, potentially swallowing those too. Consider restructuring so the fallback call only happens once per code path and errors from the fallback itself propagate normally.


Major Concerns

2. String-matching on error messages is brittle

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx:318,327

const isMissingOauthConfig = oauth2Message.includes('Missing required OAuth configuration')
// ... and ...
const isMissingOauthConfig = errorMessage.includes('Missing required OAuth configuration')

The fallback decision depends on matching a specific English-language string from the server response. This couples the frontend to the exact wording of a backend error message. If that message ever changes (e.g. a future refactor of packages/server/src/routes/oauth2/index.ts line 330), the fallback will silently stop working without any test failure or type error catching the regression.

A more robust approach would be for the server to return a machine-readable error code (e.g. errorCode: 'MISSING_OAUTH_CONFIG') alongside the human-readable message, and for the client to check that code. Since the oauth2 route already returns structured JSON ({ success: false, message: '...' }), adding an errorCode field is a low-effort, non-breaking change.

3. No user feedback when the fallback path is taken

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx:320-321, 333

When the fallback to credentialsApi.refreshAccessToken succeeds, the function continues to the success path (showSnackbar('Successfully refreshed access token', 'success')). However, when the fallback is taken silently, the user has no visibility into which refresh mechanism was actually used. This is primarily a debuggability concern: if the fallback itself fails, the error will surface to the outer catch block, which is acceptable. But having a console.warn or a brief log when the fallback is triggered would significantly aid future debugging.


Minor Issues & Suggestions

4. Duplicated isMissingOauthConfig logic

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx:316-333

The string-check isMissingOauthConfig appears in two separate code paths (inner try and outer catch) for essentially the same condition. Extracting a small helper or combining the logic into a single path would reduce surface area for bugs.

5. response?.data?.success === false is strict equality on a field that may be absent

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx:317

const oauth2Failed = response?.data?.success === false

The oauth2Api.refresh call hits /api/v1/oauth2-credential/refresh/:credentialId. Looking at the server implementation, a successful response returns { success: true, ... } and failure responses return { success: false, ... } via res.status(400).json(...). Because a 400 response will cause axios to throw rather than resolve, response?.data?.success === false inside the try block (after a resolved promise) will never actually be true under normal axios behavior. This means the inner if (oauth2Failed && isMissingOauthConfig) branch is dead code for the HTTP-error case — the catch block handles that case instead. The variable name oauth2Failed is therefore misleading. It would be clearer to document that the inner block handles 2xx responses with an explicit failure payload, and the catch handles HTTP errors.

6. No test coverage for the new fallback logic

The PR description mentions validation via pnpm --filter web build only. There are no unit or integration tests covering the two new code paths:

  • oauth2Api.refresh returns success: false with "Missing required OAuth configuration"
  • oauth2Api.refresh throws with a message containing "Missing required OAuth configuration"

These paths are non-trivial (they involve conditional branching and two different API calls) and would benefit from Jest/React Testing Library tests on GoogleDrivePicker.


Positive Observations

  • The PR is tightly scoped (single file, 22 lines) and the description clearly explains the motivation. This makes it easy to review.
  • The outer try/catch wrapper structure correctly ensures setIsRefreshing(false) runs in the finally block regardless of which code path is taken.
  • The re-auth detection logic (status === 401 || errorMessage.includes('REAUTH_REQUIRED')) is preserved unchanged, so existing re-auth flows are unaffected.
  • The fallback uses the existing credentialsApi.refreshAccessToken function already used elsewhere in the codebase (Google Drive, Gmail), keeping the approach consistent.
  • No secrets, no hardcoded credentials, no SQL queries, no XSS vectors. The multi-tenancy concern does not apply here (UI-only component, credential ID comes from props, not constructed by the user).

Checklist

  • No hardcoded secrets or credentials
  • No SQL injection or XSS risk
  • Existing re-auth handling preserved
  • enforceAbility is present on the backend routes that are called (/credentials/refresh-token uses enforceAbility('Credential'))
  • String-based error matching is brittle (see Issue Tools Sandbox #2)
  • Potential double-catch ambiguity (see Issue Answers Integration Beta v1 #1)
  • No new test coverage for the fallback paths (see Issue Added Bulk Job Scripts for Data Analysis #6)

Next Steps

  1. Address the double-catch ambiguity (Issue Answers Integration Beta v1 #1) - either restructure so the outer catch cannot re-intercept oauth2Api.refresh errors, or add a flag to distinguish which API threw.
  2. Consider adding a machine-readable errorCode field on the server response to avoid string matching (Issue Tools Sandbox #2).
  3. Add at least one unit test for each new code path (Issue Added Bulk Job Scripts for Data Analysis #6).

The fix is directionally correct and should unblock the Drive Picker in affected environments. Addressing Issue #1 and Issue #6 before merge would make the change more robust and maintainable.

@claude
Copy link
Copy Markdown

claude Bot commented Feb 20, 2026

PR Review: fix: add Google Drive OAuth refresh fallback

Reviewer: Claude Code (automated review)
Files changed: 3 | +90 / -19 lines
Linked ticket: SUPPORT-19


Summary

This PR delivers three targeted fixes to the Google Drive integration:

  1. OAuth refresh fallback (GoogleDrivePicker.jsx) - When oauth2Api.refresh fails due to a missing OAuth configuration, the component now falls back to credentialsApi.refreshAccessToken.
  2. Selected file ID normalization (GoogleDrive.ts) - Extracts a private normalizeSelectedFileIds method that handles JSON arrays, comma-separated strings, plain strings, and objects with fileId/id fields, plus deduplication via Set.
  3. State propagation fix (DocStoreInputHandler.jsx) - Replaces direct mutation (data.inputs[inputParam.name] = newValue) with handleDataChange for the Gmail and Google Drive pickers, making them consistent with how all other inputs in that component are handled.

Changes 2 and 3 are solid improvements. Change 1 works correctly but has maintainability concerns worth addressing before merge.


Critical Issues

None. No security regressions, no new unprotected routes, no multi-tenancy bypasses.


Major Concerns

1. String-matching drives critical control flow without a constant or test

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx lines 337 and 346

const isMissingOauthConfig = oauth2Message.includes('Missing required OAuth configuration')
// ...
const isMissingOauthConfig = errorMessage.includes('Missing required OAuth configuration')

The fallback decision depends on matching a substring of a server-defined English error message. The authoritative source lives in packages/server/src/routes/oauth2/index.ts line 332:

message: 'Missing required OAuth configuration: clientId, clientSecret, or refresh_token'

If this message ever changes (a wording fix, an upstream Flowise merge, or a refactor adding new fields to the message), the client fallback silently stops working. There is no test to catch this regression. At a minimum, extract the matched substring to a named constant shared between the two checks. Ideally, the server should also return a machine-readable errorCode field (e.g. errorCode: 'MISSING_OAUTH_CONFIG') so the client can check that instead.

2. The inner try/catch success-response branch is unreachable in practice

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx lines 334-343

try {
    const response = await oauth2Api.refresh(credentialId)
    const oauth2Message = response?.data?.message || ''
    const oauth2Failed = response?.data?.success === false   // <-- always false here
    const isMissingOauthConfig = oauth2Message.includes('Missing required OAuth configuration')

    if (oauth2Failed && isMissingOauthConfig) {             // <-- dead code
        await credentialsApi.refreshAccessToken({ credentialId })
    } else if (oauth2Failed) {
        throw new Error(oauth2Message || 'Error refreshing access token')
    }
} catch (error) { ... }

The server endpoint (/api/v1/oauth2-credential/refresh/:credentialId line 307 of oauth2/index.ts) returns HTTP 200 only on success. For the "missing config" case it returns HTTP 500 (res.status(500).json({ success: false, ... })). Axios throws on any non-2xx status, so oauth2Api.refresh will never resolve with { success: false } — it will throw instead. The catch block is the real execution path for this scenario, making the if (oauth2Failed && isMissingOauthConfig) branch dead code.

This is not a correctness problem because the fallback via the catch block does work, but the dead inner branch creates a false sense of coverage and makes the code harder to reason about. Consider removing the inner if (oauth2Failed) block entirely, since it can never be reached.


Minor Issues and Suggestions

3. Silent JSON parse failure in normalizeSelectedFileIds drops all items without warning

Location: packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts lines 404-407

try {
    rawItems = JSON.parse(trimmed)
} catch (error) {
    rawItems = []  // silently returns no files
}

If a user passes a malformed JSON string that looks like an array (starts with [, ends with ]) but is invalid JSON, the loader silently processes zero files and returns an empty document array. The outer init() method has no way to distinguish "user selected no files" from "JSON parsing failed." A console.warn here would significantly aid debugging when users report missing documents.

4. getCredentialDataApi.request is fire-and-forget after refresh

Location: packages/ui/src/ui-component/drive/GoogleDrivePicker.jsx line 355

getCredentialDataApi.request(credentialId)  // not awaited

This was present before this PR and is intentional (the hook manages async state). However, if this request fails after a successful fallback refresh, the token will not be updated in the component state and the user may see a stale expired-token UI even though the refresh succeeded. This is a pre-existing issue but is worth a follow-up ticket.

5. No test coverage for the new fallback branches

The PR description lists pnpm --filter web build as the only validation step. The two new code paths in handleRefreshAccessToken (fallback on success-response check, fallback on HTTP error) and the normalizeSelectedFileIds method (JSON array, comma-separated string, object with fileId/id) are not covered by any automated tests. Given that this PR targets a specific production failure (SUPPORT-19), unit tests for these branches would prevent silent regressions.

6. Missing tags: ['AAI'] on the GoogleDrive component

Location: packages/components/nodes/documentloaders/GoogleDrive/GoogleDrive.ts

The GoogleDrive_DocumentLoaders class does not include tags: string[] = ['AAI']. Per packages/components/CLAUDE.md, all TheAnswer components must include this tag. This is pre-existing and not introduced by this PR, but since the file is being touched it is a good opportunity to add it.


Positive Observations

  • The normalizeSelectedFileIds refactor is a genuine improvement. The original inline code handled only two string shapes; the new private method correctly handles JSON arrays, comma-separated strings, plain strings, object items with fileId or id properties, and deduplication via Set. This is more robust and unit-testable.
  • Switching GoogleDrivePicker and GmailLabelPicker in DocStoreInputHandler from direct mutation to handleDataChange is a correctness fix: the old pattern bypassed nodeDataChangeHandler, which is needed to trigger ReactFlow state updates for asyncMultiOptions inputs.
  • The previousCredentialIdRef approach for detecting credential changes (vs. clearing on every mount) is correct and prevents the UX regression of losing selected files when the component re-mounts with the same credential.
  • The new value sync useEffect that reads from the prop on external changes is needed for the handleDataChange fix in DocStoreInputHandler to round-trip correctly. The guard if (value === undefined) return and the Array.isArray check are appropriate.
  • The finally block correctly ensures setIsRefreshing(false) runs regardless of which code path is taken.
  • No hardcoded secrets, no SQL, no XSS vectors, no new unprotected routes.

TheAnswer-Specific Checklist

  • No new routes added (no enforceAbility check required for this diff)
  • No new database queries (no organizationId filter required)
  • Fallback endpoint (/credentials/refresh-token) uses enforceAbility('Credential') per existing route definition
  • No new migrations required
  • Error messages shown to users are friendly and do not leak internal details
  • tags: ['AAI'] missing from GoogleDrive_DocumentLoaders (pre-existing, opportunity to fix)
  • Test coverage for new fallback branches and normalizeSelectedFileIds (missing)

Next Steps

  1. High priority: Extract 'Missing required OAuth configuration' to a named constant (or add a server-side errorCode) to prevent silent fallback breakage on message changes.
  2. Medium priority: Remove the unreachable if (oauth2Failed && isMissingOauthConfig) inner branch to simplify the control flow. The catch block is the only active path.
  3. Low priority / follow-up: Add tags: ['AAI'] to GoogleDrive_DocumentLoaders and a console.warn in the JSON parse failure path of normalizeSelectedFileIds.
  4. Follow-up ticket: Add unit tests for handleRefreshAccessToken fallback branches and normalizeSelectedFileIds input variants.

The DocStoreInputHandler and GoogleDrive.ts changes are ready to merge as-is. The GoogleDrivePicker refresh logic works correctly via the catch path and unblocks SUPPORT-19, but the dead inner branch and the string-matching coupling are worth cleaning up before or shortly after merge.


Review generated by Claude Code PR Review Agent

@maxtechera maxtechera changed the title fix: add Google Drive OAuth refresh fallback fix: sync docstore drive selections Feb 20, 2026
@maxtechera maxtechera merged commit ab8f329 into staging Feb 20, 2026
7 of 8 checks passed
@maxtechera maxtechera deleted the fix/support-19-drive-picker branch February 20, 2026 01:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant