Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions extensions/cli/src/ui/FreeTrialStatus.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<FreeTrialStatus
apiClient={{ getFreeTrialStatus } as any}
model={{ provider: "anthropic", model: "claude-sonnet-4-5" } as any}
/>,
);

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(
<FreeTrialStatus
apiClient={{ getFreeTrialStatus } as any}
model={
{
provider: "continue-proxy",
model: "test-model",
apiKeyLocation: "free_trial:test",
} as any
}
/>,
);

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);
});
});
62 changes: 34 additions & 28 deletions extensions/cli/src/ui/FreeTrialStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,39 +28,50 @@ const FreeTrialStatus: React.FC<FreeTrialStatusProps> = ({
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(() => {
Expand All @@ -85,12 +96,7 @@ const FreeTrialStatus: React.FC<FreeTrialStatusProps> = ({
}, [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;
}

Expand Down
34 changes: 34 additions & 0 deletions extensions/vscode/e2e/install-marketplace-extensions.mjs
Original file line number Diff line number Diff line change
@@ -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" },
);
}
2 changes: 1 addition & 1 deletion extensions/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading