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",