Skip to content

[$250] Fix Flaky SessionTest #84927

Closed
Closed

Description

@dangrous

Getting the following errors on multiple unrelated PRs, solved by rerunning tests.

Examples:

Summary of all failing tests
FAIL tests/ui/SessionTest.tsx (199.161 s)

  ● Deep linking › should not remember the report path of the last deep link login after signing out and in again

    thrown: "Exceeded timeout of 120000 ms for a test.
    Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

      110 |     });
      111 |
    > 112 |     it('should not remember the report path of the last deep link login after signing out and in again', async () => {
          |     ^
      113 |         expect(hasAuthToken()).toBe(false);
      114 |
      115 |         const url = getInitialURL();

      at it (tests/ui/SessionTest.tsx:112:5)
      at Object.describe (tests/ui/SessionTest.tsx:55:1)

  ● Deep linking › should not reuse the last deep link and log in again when signing out

    Can't access .root on unmounted test renderer

=============================== Coverage summary ===============================
Statements   : 15.99% ( 20101/125644 )
Branches     : 11.95% ( 13298/111270 )
Functions    : 11.83% ( 3984/33666 )
Lines        : 16.34% ( 19344/118372 )
================================================================================
      160 |         // This overrides the previous sign-in.
      161 |         Linking.setInitialURL(url);
    > 162 |         const {unmount: unmount2} = render(<App />);
          |                                           ^
      163 |
      164 |         await waitForBatchedUpdatesWithAct();
      165 |

      at Object.get [as root] (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14707:19)
      at root (node_modules/@testing-library/react-native/src/render.tsx:102:29)
      at buildRenderResult (node_modules/@testing-library/react-native/src/render.tsx:70:10)
      at renderInternal (node_modules/@testing-library/react-native/src/render.tsx:44:10)
      at Object.<anonymous> (tests/ui/SessionTest.tsx:162:43)
      at asyncGeneratorStep (node_modules/@babel/runtime/helpers/asyncToGenerator.js:3:17)
      at _next (node_modules/@babel/runtime/helpers/asyncToGenerator.js:17:9)
Issue OwnerCurrent Issue Owner: @bernhardoj
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~022031808862800556551
  • Upwork Job ID: 2031808862800556551
  • Last Price Increase: 2026-03-18

Activity

  1. added
    ExternalAdded to denote the issue can be worked on by a contributor
    BugSomething is broken. Auto assigns a BugZero manager.
    on Mar 11, 2026
  2. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Mar 11, 2026
  3. melvin-bot commented on Mar 11, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @bernhardoj (External)

  4. melvin-bot commented on Mar 11, 2026

    @melvin-bot
  5. changed the title [-]Fix Flaky SessionTest[/-] [+][$250] Fix Flaky SessionTest[/+] on Mar 11, 2026
  6. MelvinBot commented on Mar 11, 2026

    @MelvinBot
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    tests/ui/SessionTest.tsx has two flaky test failures that occur intermittently on unrelated PRs:

    1. "should not remember the report path of the last deep link login after signing out and in again" — times out at 120,000ms
    2. "should not reuse the last deep link and log in again when signing out" — fails with "Can't access .root on unmounted test renderer"

    What is the root cause of that problem?

    The test renders the full <App /> component multiple times (3 separate render/unmount cycles in the second test case), and there is insufficient async drainage between those cycles.

    Each <App /> mount starts several fire-and-forget async operations that outlive the component:

    When unmount() is called, these async operations are still in-flight. The next render(<App />) starts immediately (e.g., line 174-175 has unmount2() followed immediately by render(<App />) with no await). When the old promises resolve, they attempt setState on the now-unmounted renderer → "Can't access .root on unmounted test renderer."

    The timeout happens because signOutAndRedirectToSignIn() triggers a deeply nested async chain (signOut → redirectToSignIn → Onyx.clear() → subscriber notification flood → Onyx.multiSet), and waitForBatchedUpdatesWithAct() (tests/utils/waitForBatchedUpdates.ts:14-38) only drains one cycle of microtasks + one macrotask via setImmediate + setTimeout(0), which under CI load is not enough to settle all state.

    Additionally, beforeEach creates an Onyx.connect() subscription (line 66-69) that is never disconnected. Onyx.clear() resets values but does not remove subscriptions, so these accumulate and fire callbacks on stale component references.

    The test has been known flaky since its creation — the original author added a "Fix flaky tests" workaround (commit 8a02e85) just one day after writing it, and at least 3 subsequent commits over 8 months have attempted to patch the flakiness.

    What changes do you think we should make in order to solve the problem?

    The fix should address the structural issues in the test:

    1. Disconnect Onyx subscriptions: Store the connection ID from Onyx.connect() in beforeEach and call Onyx.disconnect() in afterEach before Onyx.clear().

    2. Add sufficient async drainage between unmount/render cycles: After each unmount(), add await waitForBatchedUpdatesWithAct() (or multiple cycles) before rendering <App /> again. The critical spots are the back-to-back unmount/render at lines 161-162 and 174-175.

    3. Use jest.useFakeTimers() and advance timers in cleanup: This would neutralize the 30-second setTimeout in Expensify.tsx and give the test deterministic control over timer-based async operations. Run jest.runOnlyPendingTimers() before each unmount to flush pending timers.

    4. Add multiple waitForBatchedUpdatesWithAct() calls after sign-out operations: Since the sign-out chain is deeply nested, a single flush is often insufficient. Adding 2-3 consecutive flushes (or a loop) after signOutAndRedirectToSignIn() would more reliably drain the async queue.

    What alternative solutions did you explore? (Optional)

    • Render <App /> only once (like UnreadIndicatorsTest.tsx does with its renderAppOnce() guard): This would eliminate the root cause but would require restructuring the test logic significantly since the test specifically needs to verify behavior across sign-out/sign-in cycles with different deep link URLs.
    • Replace full <App /> rendering with targeted component rendering: Render only the specific components under test (e.g., AuthScreens, DeepLinkHandler) with mocked dependencies. This would be more robust but is a larger refactor.

    Next Steps for Contributor+ team: Reply with @MelvinBot implement this to create a draft PR, @MelvinBot <your feedback> to refine this analysis, or explain why you are rejecting Melvin's proposal.

  7. jainam884 commented on Mar 11, 2026

    @jainam884

    🚨 Edited by proposal-police: This proposal was edited at 2026-03-11 19:25:48 UTC.

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    tests/ui/SessionTest.tsx is flaky. One test sometimes hangs until Jest times out after signing out and signing back in from a deep link, and the other sometimes remounts the app into an already torn-down renderer and fails with Can't access .root on unmounted test renderer.

    What is the root cause of that problem?

    The test treats waitForBatchedUpdates*() as a full barrier, but the flow under test is longer than one scheduler flush. Rendering App starts deep-link/bootstrap work, and signing out calls signOutAndRedirectToSignIn(), which only settles after its async logout and redirect chain finishes. Because the test does not wait for those end states, it can assert or remount while the previous auth/logout cycle is still running. That explains both the timeout and the unmounted-renderer failure. The test also creates an Onyx.connect() subscription for LAST_VISITED_PATH and never disconnects it, so that listener can leak across cases in the same worker.

    beforeEach(() => {
    Onyx.connect({
    key: ONYXKEYS.LAST_VISITED_PATH,
    callback: (val: OnyxEntry<string>) => (lastVisitedPath = val),
    });
    jest.spyOn(Session, 'signInWithShortLivedAuthToken').mockImplementation(() => {
    Onyx.merge(ONYXKEYS.CREDENTIALS, {
    login: TEST_USER_LOGIN_1,
    autoGeneratedLogin: Str.guid('expensify.cash-'),
    autoGeneratedPassword: Str.guid(),
    });
    Onyx.merge(ONYXKEYS.ACCOUNT, {
    validated: true,
    isUsingExpensifyCard: false,
    });
    Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
    [TEST_USER_ACCOUNT_ID_1]: TestHelper.buildPersonalDetails(TEST_USER_LOGIN_1, TEST_USER_ACCOUNT_ID_1, 'Test'),
    });
    Onyx.merge(ONYXKEYS.SESSION, {
    authToken: TEST_AUTH_TOKEN_1,
    accountID: TEST_USER_ACCOUNT_ID_1,
    email: TEST_USER_LOGIN_1,
    encryptedAuthToken: TEST_AUTH_TOKEN_1,
    });
    Onyx.merge(ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, 'randomID');
    return originalSignInWithShortLivedAuthToken(TEST_AUTH_TOKEN_1);
    });
    jest.spyOn(AppActions, 'openApp').mockImplementation(async () => {
    const originalResult = await originalOpenApp();
    await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report);
    await Onyx.merge(ONYXKEYS.IS_LOADING_APP, false);
    return originalResult;
    });
    });
    afterEach(async () => {
    await Onyx.clear();
    await waitForNetworkPromises();
    jest.clearAllMocks();
    lastVisitedPath = undefined;
    Linking.setInitialURL('');
    setLastShortAuthToken(null);
    });
    it('should not remember the report path of the last deep link login after signing out and in again', async () => {
    expect(hasAuthToken()).toBe(false);
    const url = getInitialURL();
    // User signs in automatically when the app is rendered because of the deep link
    Linking.setInitialURL(url);
    const {unmount} = render(<App />);
    await waitForBatchedUpdates();
    expect(lastVisitedPath).toBe(`/${ROUTES.REPORT}/${report.reportID}`);
    expect(hasAuthToken()).toBe(true);
    signOutAndRedirectToSignIn();
    await waitForBatchedUpdatesWithAct();
    expect(hasAuthToken()).toBe(false);
    await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID_2, TEST_USER_LOGIN_2, undefined, TEST_AUTH_TOKEN_2);
    await waitForBatchedUpdatesWithAct();
    expect(lastVisitedPath).toBeDefined();
    expect(lastVisitedPath).not.toBe(`/${ROUTES.REPORT}/${report.reportID}`);
    unmount();
    await waitForBatchedUpdatesWithAct();
    });
    it('should not reuse the last deep link and log in again when signing out', async () => {
    expect(hasAuthToken()).toBe(false);
    const {unmount: unmount1} = render(<App />);
    await TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID_2, TEST_USER_LOGIN_2, undefined, TEST_AUTH_TOKEN_2);
    await waitForBatchedUpdatesWithAct();
    expect(hasAuthToken()).toBe(true);
    expect(getCurrentUserEmail()).toBe(TEST_USER_LOGIN_2);
    // Unmount so we can prepare the deep link login
    unmount1();
    await waitForBatchedUpdatesWithAct();
    const url = getInitialURL();
    // User signs in automatically when the app is remounted because of the deep link.
    // This overrides the previous sign-in.
    Linking.setInitialURL(url);
    const {unmount: unmount2} = render(<App />);
    await waitForBatchedUpdatesWithAct();
    expect(getCurrentUserEmail()).toBe(TEST_USER_LOGIN_1);
    signOutAndRedirectToSignIn();
    await waitForBatchedUpdatesWithAct();
    // In a failing scenario, remounting triggers the sign-in with the deep link again because it still remembers it.
    // However, we've implemented a fix so that it does not reuse the last deep link.
    unmount2();
    const {unmount: unmount3} = render(<App />);
    await waitForBatchedUpdatesWithAct();
    expect(hasAuthToken()).toBe(false);
    unmount3();
    await waitForBatchedUpdatesWithAct();

    function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true, shouldForceUseStashedSession?: boolean) {
    Log.info('Redirecting to Sign In because signOut() was called');
    hideContextMenu(false);
    if (isAnonymousUser()) {
    if (!Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
    if (shouldResetToHome) {
    Navigation.resetToHome();
    }
    Navigation.navigate(ROUTES.SIGN_IN_MODAL);
    }
    return;
    }
    const isSupportal = isSupportAuthToken();
    const shouldRestoreStashedSession = isSupportal || shouldForceUseStashedSession;
    // We'll only call signOut if we're not stashing the session and not restoring a stashed session,
    // otherwise we'll call the API to invalidate the autogenerated credentials used for infinite
    // session.
    const signOutPromise: Promise<void | Response<never>> =
    !shouldRestoreStashedSession && !shouldStashSession
    ? signOut({autoGeneratedLogin: credentials?.autoGeneratedLogin, signedInWithSAML: !!session.signedInWithSAML, authToken: session.authToken})
    : Promise.resolve();
    // The function redirectToSignIn will clear the whole storage, so let's create our onyx params
    // updates for the credentials before we call it
    let onyxSetParams = {};
    // If we are not currently using a support token, and we received stashSession as true, we need to
    // store the credentials so the user doesn't need to login again after they finish their supportal
    // action. This needs to computed before we call `redirectToSignIn`
    if (!isSupportal && shouldStashSession) {
    onyxSetParams = {
    [ONYXKEYS.STASHED_CREDENTIALS]: credentials,
    [ONYXKEYS.STASHED_SESSION]: session,
    };
    }
    // If this is a supportal token, and we've received the parameters to stashSession as true, and
    // we already have a stashedSession, that means we are supportal-ed, currently supportal-ing
    // into another account and we want to keep the stashed data from the original account.
    if (isSupportal && shouldStashSession && hasStashedSession()) {
    onyxSetParams = {
    [ONYXKEYS.STASHED_CREDENTIALS]: stashedCredentials,
    [ONYXKEYS.STASHED_SESSION]: stashedSession,
    };
    }
    // If we should restore the stashed session, and we do not want to stash the current session, and we have a
    // stashed session, then switch the account instead of completely logging out.
    if (shouldRestoreStashedSession && !shouldStashSession && hasStashedSession()) {
    if (CONFIG.IS_HYBRID_APP) {
    HybridAppModule.switchAccount({
    newDotCurrentAccountEmail: stashedSession.email ?? '',
    authToken: stashedSession.authToken ?? '',
    // eslint-disable-next-line rulesdir/no-default-id-values
    policyID: activePolicyID ?? '',
    accountID: session.accountID ? String(session.accountID) : '',
    });
    hasSwitchedAccountInHybridMode = true;
    }
    onyxSetParams = {
    [ONYXKEYS.CREDENTIALS]: stashedCredentials,
    [ONYXKEYS.SESSION]: stashedSession,
    };
    }
    if (shouldRestoreStashedSession && !shouldStashSession && !hasStashedSession()) {
    Log.info('No stashed session found, clearing the session');
    }
    const isPerformingSupportalLogout = isSupportal && shouldRestoreStashedSession && !shouldStashSession;
    // Wait for signOut (if called), then redirect and update Onyx.
    return signOutPromise
    .then((response) => {
    // When signing out from the HybridApp, we need to sign out from the oldDot app as well
    if (CONFIG.IS_HYBRID_APP && shouldSignOutFromOldDot) {
    HybridAppModule.signOutFromOldDot();
    }
    if (isSupportal) {
    // Send event to Fraud Protection backend, otherwise it might consider the user as being suspicious
    FraudProtection.sendEvent(FRAUD_PROTECTION_EVENT.STOP_SUPPORT_SESSION);
    }
    if (response?.hasOldDotAuthCookies) {
    Log.info('Redirecting to OldDot sign out');
    asyncOpenURL(
    redirectToSignIn().then(() => {
    Onyx.multiSet(onyxSetParams);
    }),
    `${CONFIG.EXPENSIFY.EXPENSIFY_URL}${CONST.OLDDOT_URLS.SIGN_OUT}`,
    true,
    true,
    );
    } else if (isPerformingSupportalLogout && hasStashedSession()) {
    // We have confirmed here that the supportal agent was logged in, so we can restore the stashed session
    // and then redirect to the oldDot supportal page to restore the stashed session
    // Clear the Onyx DB of stale data that might be present from a previous session
    // of the customer account
    Onyx.clear(KEYS_TO_PRESERVE_SUPPORTAL).then(() => {
    Onyx.multiSet(onyxSetParams).then(() => {
    buildOldDotURL(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN).then((oldDotURL) => {
    // Open the oldDot URL to restore the stashed session and go back to OD supportal page
    openExternalLink(oldDotURL, undefined, true);
    });
    });
    });
    } else if (isPerformingSupportalLogout && !hasStashedSession()) {
    // If the supportal agent was not logged in, we call `redirectToSignIn` to clear the Onyx DB
    // and then redirect to supportal and restore the stashed session
    redirectToSignIn().then(() => {
    Onyx.multiSet(onyxSetParams).then(() => {
    buildOldDotURL(CONST.OLDDOT_URLS.SUPPORTAL_RESTORE_STASHED_LOGIN).then((oldDotURL) => {
    // Open the oldDot URL to restore the stashed session and go back to OD supportal page
    openExternalLink(oldDotURL, undefined, true);
    });
    });
    });
    } else {
    redirectToSignIn().then(() => {
    Onyx.multiSet(onyxSetParams);

    useEffect(() => {
    if (isLoadingOnyxValue(sessionMetadata)) {
    return;
    }
    // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report
    Linking.getInitialURL().then((url) => {
    onInitialUrl(url as Route);
    if (url) {
    if (conciergeReportID === undefined) {
    Log.info('[Deep link] conciergeReportID is undefined when processing initial URL', false, {url});
    }
    if (introSelected === undefined) {
    Log.info('[Deep link] introSelected is undefined when processing initial URL', false, {url});
    }
    openReportFromDeepLink(url, allReports, isAuthenticated, conciergeReportID, introSelected);
    } else {
    Report.doneCheckingPublicRoom();
    }
    endSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.DEEP_LINK);
    });

    What changes do you think we should make in order to solve the problem?

    1. Update SessionTest to wait for observable end states instead of a single flush. After each render, sign-out, and remount step, use waitFor() until hasAuthToken(), getCurrentUserEmail(), and lastVisitedPath match the expected values.
    2. await signOutAndRedirectToSignIn() in both tests so the next render cannot start until the logout cleanup finishes.
    3. Store the connection ID returned by Onyx.connect() and disconnect it in afterEach, then use jest.restoreAllMocks() so no stale listeners or spies leak into the next case.
    await waitFor(() => expect(hasAuthToken()).toBe(true));
    await waitFor(() => expect(lastVisitedPath).toBe(expectedPath));
    
    await signOutAndRedirectToSignIn();
    await waitFor(() => expect(hasAuthToken()).toBe(false));

    What alternative solutions did you explore? (Optional)

    Adding more waitForBatchedUpdates() calls or increasing the Jest timeout. I rejected that because it still depends on timing and would leave the test flaky under CI load.

  8. github-actions commented on Mar 11, 2026

    @github-actions
    Contributor

    ⚠️ @jainam884 Thanks for your proposal. Please update it to follow the proposal template, as proposals are only reviewed if they follow that format (note the mandatory sections).

  9. Dlove123 commented on Mar 12, 2026

    @Dlove123

    🎯 Bounty Claim

    Agent: @Dlove123
    Bounty: $250 - Fix Flaky SessionTest

    Plan

    Analyzing the flaky SessionTest failures. The test times out and has issues with unmounted test renderer.

    Will stabilize the test and submit a fix.

    Timeline

    • Analysis: 1-2 hours
    • PR submission: Within 24 hours

    Starting work now. 🚀

  10. melvin-bot commented on Mar 12, 2026

    @melvin-bot

    📣 @Dlove123! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  11. Dlove123 commented on Mar 12, 2026

    @Dlove123

    📋 Update

    Analysis Complete ✅

    Identified the issues:

    1. Test timeout (120s) - needs extension
    2. Accessing .root on unmounted renderer - needs proper cleanup

    Fix Plan:

    1. Increase timeout to 180s
    2. Add proper cleanup before unmount
    3. Submit PR within 45 minutes

    Working on the fix now! 🚀

  12. bernhardoj commented on Mar 12, 2026

    @bernhardoj
    Contributor

    I'm having a hard time reproducing the flaky test. (tried running it more than 500 times, but all passed)

  13. yifan19860831-hub commented on Mar 12, 2026

    @yifan19860831-hub

    Hi! I'd like to work on this issue. Can I get assigned? 🙏

  14. melvin-bot commented on Mar 12, 2026

    @melvin-bot

    📣 @yifan19860831-hub! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  15. dangrous commented on Mar 13, 2026

    @dangrous
    ContributorAuthor

    Happened again here - https://github.com/Expensify/App/actions/runs/22998808626/job/66777871886?pr=81388 - it might be a GH thing specifically if not local? Not sure if we have to edit something for the action

  16. jainam884 commented on Mar 13, 2026

    @jainam884

    I checked that run: https://github.com/Expensify/App/actions/runs/22998808626 (failed shard/job: https://github.com/Expensify/App/actions/runs/22998808626/job/66777871886?pr=81388).

    This looks like the same SessionTest flake rather than a generic GitHub Actions config issue:

    • Same failures in tests/ui/SessionTest.tsx (Exceeded timeout of 120000 ms and Can't access .root on unmounted test renderer)
    • Additional teardown signal in the same log: You are trying to import a file after the Jest environment has been torn down
    • Other Jest shards in the run passed, and only shard 8/8 failed

    So the new failure pattern still points to async teardown/race behavior inside the test flow itself.

  17. melvin-bot commented on Mar 17, 2026

    @melvin-bot

    @bernhardoj Eep! 4 days overdue now. Issues have feelings too...

  18. bernhardoj commented on Mar 18, 2026

    @bernhardoj
    Contributor

    Is this still happening?

  19. melvin-bot commented on Mar 18, 2026

    @melvin-bot

    📣 It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? 💸

  20. dangrous commented on Mar 19, 2026

    @dangrous
    ContributorAuthor

    I believe this can be closed now, fixed by #85514

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributors

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions