diff --git a/extensions/cli/src/ui/FreeTrialStatus.test.tsx b/extensions/cli/src/ui/FreeTrialStatus.test.tsx new file mode 100644 index 00000000000..7b76ae82a02 --- /dev/null +++ b/extensions/cli/src/ui/FreeTrialStatus.test.tsx @@ -0,0 +1,68 @@ +import { render } from "ink-testing-library"; +import React from "react"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { FreeTrialStatus } from "./FreeTrialStatus.js"; + +describe("FreeTrialStatus", () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + process.env.NODE_ENV = originalNodeEnv; + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + it("does not fetch or poll for non-free-trial models", async () => { + vi.useFakeTimers(); + const getFreeTrialStatus = vi.fn().mockResolvedValue({ + optedInToFreeTrial: true, + chatCount: 1, + chatLimit: 10, + }); + + render( + , + ); + + await vi.runAllTimersAsync(); + + expect(getFreeTrialStatus).not.toHaveBeenCalled(); + }); + + it("fetches immediately and polls every five seconds for free-trial models", async () => { + vi.useFakeTimers(); + process.env.NODE_ENV = "development"; + const getFreeTrialStatus = vi.fn().mockResolvedValue({ + optedInToFreeTrial: true, + chatCount: 1, + chatLimit: 10, + }); + + render( + , + ); + + await Promise.resolve(); + await Promise.resolve(); + expect(getFreeTrialStatus).toHaveBeenCalledTimes(1); + + await vi.advanceTimersByTimeAsync(5000); + expect(getFreeTrialStatus).toHaveBeenCalledTimes(2); + + await vi.advanceTimersByTimeAsync(10000); + expect(getFreeTrialStatus).toHaveBeenCalledTimes(4); + }); +}); diff --git a/extensions/cli/src/ui/FreeTrialStatus.tsx b/extensions/cli/src/ui/FreeTrialStatus.tsx index 5a2c7c78d0b..a275b0820bf 100644 --- a/extensions/cli/src/ui/FreeTrialStatus.tsx +++ b/extensions/cli/src/ui/FreeTrialStatus.tsx @@ -28,39 +28,50 @@ const FreeTrialStatus: React.FC = ({ null, ); const [loading, setLoading] = useState(true); + const shouldFetchStatus = !!apiClient && isModelUsingFreeTrial(model); - const fetchStatus = async () => { - try { - if (!apiClient) { - setStatus(null); - setLoading(false); - return; - } - - const response = await apiClient.getFreeTrialStatus(); - setStatus(response); - setLoading(false); - } catch { - // Silently handle errors - component returns null if no status + useEffect(() => { + if (!shouldFetchStatus) { setStatus(null); setLoading(false); + return; } - }; - useEffect(() => { - // Initial fetch - fetchStatus(); + let isMounted = true; + + const fetchStatus = async () => { + try { + const response = await apiClient.getFreeTrialStatus(); + if (!isMounted) { + return; + } + setStatus(response); + setLoading(false); + } catch { + if (!isMounted) { + return; + } + setStatus(null); + setLoading(false); + } + }; + + setLoading(true); + void fetchStatus(); - // Don't poll in test environment if (process.env.NODE_ENV === "test") { - return; + return () => { + isMounted = false; + }; } - // Poll every 5 seconds const interval = setInterval(fetchStatus, 5000); - return () => clearInterval(interval); - }, []); + return () => { + isMounted = false; + clearInterval(interval); + }; + }, [apiClient, shouldFetchStatus]); // Check if user has maxed out their free trial and notify parent useEffect(() => { @@ -85,12 +96,7 @@ const FreeTrialStatus: React.FC = ({ }, [status, loading, onTransitionStateChange, model]); // Don't render anything while loading or if no status - if ( - loading || - !status || - !status.optedInToFreeTrial || - !isModelUsingFreeTrial(model) - ) { + if (loading || !status || !status.optedInToFreeTrial || !shouldFetchStatus) { return null; } diff --git a/extensions/vscode/e2e/install-marketplace-extensions.mjs b/extensions/vscode/e2e/install-marketplace-extensions.mjs new file mode 100644 index 00000000000..dbb3b05aac2 --- /dev/null +++ b/extensions/vscode/e2e/install-marketplace-extensions.mjs @@ -0,0 +1,34 @@ +import { execFileSync } from "node:child_process"; + +const extensionsDir = "./e2e/.test-extensions"; +const storageDir = "./e2e/storage"; +const remoteExtensions = [ + "ms-vscode-remote.remote-ssh", + "ms-vscode-remote.remote-containers", + "ms-vscode-remote.remote-wsl", +]; + +if (process.env.IGNORE_SSH_TESTS === "true") { + console.log( + "Skipping Remote-* marketplace extension installs because IGNORE_SSH_TESTS=true.", + ); + process.exit(0); +} + +const extestCommand = process.platform === "win32" ? "extest.cmd" : "extest"; + +for (const extensionId of remoteExtensions) { + console.log(`Installing ${extensionId} from the VS Code marketplace...`); + execFileSync( + extestCommand, + [ + "install-from-marketplace", + extensionId, + "--extensions_dir", + extensionsDir, + "--storage", + storageDir, + ], + { stdio: "inherit" }, + ); +} diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index cfe48996f18..e13bc1f3ad1 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -685,7 +685,7 @@ "e2e:sign-vscode": "codesign --entitlements entitlements.plist --deep --force -s - './e2e/storage/Visual Studio Code.app'", "e2e:copy-vsix": "chmod +x ./e2e/get-latest-vsix.sh && bash ./e2e/get-latest-vsix.sh", "e2e:install-vsix": "extest install-vsix -f ./e2e/vsix/continue.vsix --extensions_dir ./e2e/.test-extensions --storage ./e2e/storage", - "e2e:install-extensions": "extest install-from-marketplace ms-vscode-remote.remote-ssh --extensions_dir ./e2e/.test-extensions --storage ./e2e/storage && extest install-from-marketplace ms-vscode-remote.remote-containers --extensions_dir ./e2e/.test-extensions --storage ./e2e/storage && extest install-from-marketplace ms-vscode-remote.remote-wsl --extensions_dir ./e2e/.test-extensions --storage ./e2e/storage", + "e2e:install-extensions": "node ./e2e/install-marketplace-extensions.mjs", "e2e:test": "NODE_ENV=e2e extest run-tests ${TEST_FILE:-'./e2e/_output/tests/*.test.js'} --code_settings settings.json --extensions_dir ./e2e/.test-extensions --storage ./e2e/storage", "e2e:clean": "rm -rf ./e2e/_output ./e2e/storage", "e2e:all": "npm run e2e:build && npm run e2e:compile && npm run e2e:create-storage && npm run e2e:get-chromedriver && npm run e2e:get-vscode && npm run e2e:sign-vscode && npm run e2e:copy-vsix && npm run e2e:install-vsix && npm run e2e:install-extensions && CONTINUE_GLOBAL_DIR=e2e/test-continue npm run e2e:test && npm run e2e:clean",