Skip to content

Commit 0f54a67

Browse files
rajbosCopilot
andauthored
feat: load and log Copilot plan info from copilot_internal/user API (#687)
* feat: load and log Copilot plan info from copilot_internal/user API After a GitHub session is established (startup restore, manual sign-in, or external session change), fire-and-forget fetch of https://api.github.com/copilot_internal/user to log the user's Copilot plan details (plan type, IDE chat, agent mode, public code suggestions, PR summaries) to the output channel. - Add CopilotPlanInfo type, fetchCopilotPlanInfoPage (internal) and fetchCopilotPlanInfo (exported with injectable fetcher for tests) to githubPrService.ts - Add CopilotTokenTracker.loadAndLogCopilotPlanInfo() that calls the fetcher and logs key plan fields; non-2xx responses and network errors are surfaced as warnings (best-effort, no impact on startup) - Wire void calls from restoreGitHubSession, authenticateWithGitHub, and the onDidChangeSessions handler so plan info refreshes on session change - Add 6 unit tests covering success, 401/403, network error, invalid response format, and partial data Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add copilotPlans.json with plan names and monthly premium request allotments Add a new data file mapping copilot_plan API values to display names, monthly prices, and premium request allotments (as of 2026-04-27): free → Copilot Free → 50 premium requests/month individual → Copilot Pro → 300 premium requests/month pro → Copilot Pro → 300 premium requests/month pro_plus → Copilot Pro+ → 1500 premium requests/month business → Copilot Business → 300 premium requests/month enterprise → Copilot Enterprise → 1000 premium requests/month Update loadAndLogCopilotPlanInfo() to look up the plan from the new file and include the human-readable name and credit allotment in the output log line, e.g.: Copilot plan: Copilot Enterprise (enterprise) Monthly premium requests: 1,000/month Document the new file in vscode-extension/src/README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: add monthlyAiCreditsUsd to copilotPlans.json and log it Each plan now records the dollar value of AI credits included per month (1 AI credit = \.01): free → \ (none) pro → \/month pro_plus → \/month business → \/month enterprise → \/month Update loadAndLogCopilotPlanInfo() to include the AI credits line: Monthly AI credits: \/month included Update README table and metadata notes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: show Copilot plan name and included AI credits in details table Store the resolved plan from copilot_internal/user on the tracker (_copilotPlanResolved) and pass it to the details webview via window.__INITIAL_DETAILS__.copilotPlan. In the metrics table, two new rows appear after 'Estimated cost (TBB)' when a plan is known — only for the 'Last 30 Days' and 'Previous Month' columns (Today/Projected show '—' since the allotment is per-month): 🏷️ Copilot plan — Copilot Enterprise Copilot Enterprise — 💳 Included AI credits — \ \ — Both rows include tooltips. Rows are omitted entirely when the plan has not yet been loaded (e.g. user not signed in to GitHub). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: merge Copilot plan and AI credits into a single table row Combine the two rows into one: 'Copilot plan' now shows 'Copilot Enterprise (\ credits)' in the Last 30 Days and Previous Month columns instead of separate plan/credits rows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: move Copilot plan info to label column Show 'Copilot Enterprise (\ credits/month)' in the row label and '—' in all value columns for a cleaner look. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3023dc1 commit 0f54a67

6 files changed

Lines changed: 289 additions & 1 deletion

File tree

vscode-extension/src/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@
22

33
This directory contains JSON configuration files for the GitHub Copilot Token Tracker extension.
44

5+
## copilotPlans.json
6+
7+
Contains GitHub Copilot plan definitions — the `plans` object keys match the `copilot_plan` value returned by the `copilot_internal/user` API endpoint.
8+
9+
**Structure:**
10+
```json
11+
{
12+
"plans": {
13+
"<plan-id>": {
14+
"name": "Display name",
15+
"monthlyPricePerUser": 10,
16+
"monthlyPremiumRequests": 300,
17+
"codeCompletionsPerMonth": null,
18+
"description": "..."
19+
}
20+
}
21+
}
22+
```
23+
24+
- `monthlyPremiumRequests` — included allotment per user per month; extra requests are $0.04 each
25+
- `monthlyAiCreditsUsd` — dollar value of AI credits included with the plan (1 AI credit = $0.01); `0` = none included
26+
- `codeCompletionsPerMonth` — tab completions limit; `null` = unlimited (paid plans)
27+
28+
**Current plans:**
29+
30+
| ID | Name | $/user/mo | AI credits/mo | Premium requests/mo |
31+
|----|------|-----------|---------------|---------------------|
32+
| `free` | Copilot Free | $0 | none | 50 |
33+
| `individual` / `pro` | Copilot Pro | $10 | $10 | 300 |
34+
| `pro_plus` | Copilot Pro+ | $39 | $39 | 1,500 |
35+
| `business` | Copilot Business | $19 | $19 | 300 |
36+
| `enterprise` | Copilot Enterprise | $39 | $39 | 1,000 |
37+
38+
**How to update:** Edit the `plans` object when GitHub changes pricing or adds plans. Update `metadata.lastUpdated` and reference the [official plans page](https://docs.github.com/en/copilot/get-started/plans).
39+
540
## tokenEstimators.json
641

742
Contains character-to-token ratio estimators for different AI models. These ratios are used to estimate token counts from text length.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"description": "GitHub Copilot plan definitions. Keys match the value of the 'copilot_plan' field returned by the copilot_internal/user API endpoint.",
4+
"metadata": {
5+
"lastUpdated": "2026-04-27",
6+
"source": "https://docs.github.com/en/copilot/get-started/plans",
7+
"notes": "monthlyPremiumRequests is the included allotment per user per month. null means unlimited. Extra premium requests beyond the allotment are billed at $0.04 each. Code completions (tab completions) do not consume premium requests on paid plans. monthlyAiCreditsUsd is the dollar value of AI credits included with the plan (1 AI credit = $0.01)."
8+
},
9+
"plans": {
10+
"free": {
11+
"name": "Copilot Free",
12+
"monthlyPricePerUser": 0,
13+
"monthlyAiCreditsUsd": 0,
14+
"monthlyPremiumRequests": 50,
15+
"codeCompletionsPerMonth": 2000,
16+
"description": "Limited plan for light use and evaluation"
17+
},
18+
"individual": {
19+
"name": "Copilot Pro",
20+
"monthlyPricePerUser": 10,
21+
"monthlyAiCreditsUsd": 10,
22+
"monthlyPremiumRequests": 300,
23+
"codeCompletionsPerMonth": null,
24+
"description": "For individual developers (legacy plan ID, same as 'pro')"
25+
},
26+
"pro": {
27+
"name": "Copilot Pro",
28+
"monthlyPricePerUser": 10,
29+
"monthlyAiCreditsUsd": 10,
30+
"monthlyPremiumRequests": 300,
31+
"codeCompletionsPerMonth": null,
32+
"description": "For individual developers"
33+
},
34+
"pro_plus": {
35+
"name": "Copilot Pro+",
36+
"monthlyPricePerUser": 39,
37+
"monthlyAiCreditsUsd": 39,
38+
"monthlyPremiumRequests": 1500,
39+
"codeCompletionsPerMonth": null,
40+
"description": "For power users needing more premium requests and the broadest model selection"
41+
},
42+
"business": {
43+
"name": "Copilot Business",
44+
"monthlyPricePerUser": 19,
45+
"monthlyAiCreditsUsd": 19,
46+
"monthlyPremiumRequests": 300,
47+
"codeCompletionsPerMonth": null,
48+
"description": "For organizations; includes policy controls and audit logs"
49+
},
50+
"enterprise": {
51+
"name": "Copilot Enterprise",
52+
"monthlyPricePerUser": 39,
53+
"monthlyAiCreditsUsd": 39,
54+
"monthlyPremiumRequests": 1000,
55+
"codeCompletionsPerMonth": null,
56+
"description": "For large organizations on GitHub Enterprise Cloud; includes custom models and expanded controls"
57+
}
58+
}
59+
}

vscode-extension/src/extension.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import tokenEstimatorsData from './tokenEstimators.json';
77
import modelPricingData from './modelPricing.json';
88
import toolNamesData from './toolNames.json';
99
import customizationPatternsData from './customizationPatterns.json';
10+
import copilotPlansData from './copilotPlans.json';
1011
import { REPO_HYGIENE_SKILL } from './backend/repoHygieneSkill';
1112
import { BackendFacade } from './backend/facade';
1213
import { BackendCommandHandler } from './backend/commands';
@@ -18,6 +19,8 @@ import {
1819
detectAiType,
1920
discoverGitHubRepos,
2021
fetchRepoPrs,
22+
fetchCopilotPlanInfo,
23+
type CopilotPlanInfo,
2124
type RepoPrDetail,
2225
type RepoPrInfo,
2326
type RepoPrStatsResult,
@@ -281,6 +284,8 @@ class CopilotTokenTracker implements vscode.Disposable {
281284
private _sessionRestorePromise: Promise<void> | undefined;
282285
/** True when the user explicitly signed out from our extension this VS Code session. Gated by globalState so it survives reloads. */
283286
private _githubSignedOutByUser: boolean = false;
287+
/** Resolved Copilot plan details fetched from copilot_internal/user after sign-in. */
288+
private _copilotPlanResolved: { planId: string; planName: string; monthlyAiCreditsUsd: number; monthlyPremiumRequests: number | null } | undefined;
284289

285290
// Cached PR stats result for the repos tab
286291
private _lastRepoPrStats?: RepoPrStatsResult;
@@ -899,6 +904,7 @@ class CopilotTokenTracker implements vscode.Disposable {
899904
this.githubSession = session;
900905
await this.context.globalState.update('github.authenticated', true);
901906
await this.context.globalState.update('github.username', session.account.label);
907+
void this.loadAndLogCopilotPlanInfo();
902908
} else {
903909
this.githubSession = undefined;
904910
await this.context.globalState.update('github.authenticated', false);
@@ -1101,6 +1107,7 @@ class CopilotTokenTracker implements vscode.Disposable {
11011107
vscode.window.showInformationMessage(`GitHub authentication successful! Logged in as ${session.account.label}`);
11021108
await this.context.globalState.update('github.authenticated', true);
11031109
await this.context.globalState.update('github.username', session.account.label);
1110+
void this.loadAndLogCopilotPlanInfo();
11041111
}
11051112
} catch (error) {
11061113
this.error('GitHub authentication failed:', error);
@@ -1296,6 +1303,7 @@ class CopilotTokenTracker implements vscode.Disposable {
12961303
this.log(`✅ GitHub session found for ${session.account.label}`);
12971304
await this.context.globalState.update('github.authenticated', true);
12981305
await this.context.globalState.update('github.username', session.account.label);
1306+
void this.loadAndLogCopilotPlanInfo();
12991307
} else {
13001308
const wasAuthenticated = this.context.globalState.get<boolean>('github.authenticated', false);
13011309
if (wasAuthenticated) {
@@ -1312,6 +1320,47 @@ class CopilotTokenTracker implements vscode.Disposable {
13121320
}
13131321
}
13141322

1323+
/**
1324+
* Fetch and log Copilot plan information for the authenticated user.
1325+
* Best-effort: silently skips if not authenticated or if the endpoint is unavailable.
1326+
*/
1327+
private async loadAndLogCopilotPlanInfo(): Promise<void> {
1328+
if (!this.githubSession) { return; }
1329+
try {
1330+
const { planInfo, statusCode, error } = await fetchCopilotPlanInfo(this.githubSession.accessToken);
1331+
if (error || !planInfo) {
1332+
this.warn(`Copilot plan info unavailable (HTTP ${statusCode ?? 'n/a'}): ${error ?? 'no data'}`);
1333+
return;
1334+
}
1335+
const planId = planInfo.copilot_plan as string | undefined;
1336+
const plans = copilotPlansData.plans as Record<string, { name: string; monthlyPremiumRequests: number | null; monthlyPricePerUser: number; monthlyAiCreditsUsd: number }>;
1337+
const knownPlan = planId ? plans[planId] : undefined;
1338+
const planLabel = knownPlan ? `${knownPlan.name} (${planId})` : (planId ?? 'unknown');
1339+
this.log(`Copilot plan: ${planLabel}`);
1340+
if (knownPlan) {
1341+
const credits = knownPlan.monthlyPremiumRequests !== null ? `${knownPlan.monthlyPremiumRequests.toLocaleString()}/month` : 'unlimited';
1342+
this.log(` Monthly premium requests: ${credits}`);
1343+
const aiCredits = knownPlan.monthlyAiCreditsUsd > 0 ? `$${knownPlan.monthlyAiCreditsUsd}/month included` : 'none';
1344+
this.log(` Monthly AI credits: ${aiCredits}`);
1345+
this._copilotPlanResolved = {
1346+
planId: planId!,
1347+
planName: knownPlan.name,
1348+
monthlyAiCreditsUsd: knownPlan.monthlyAiCreditsUsd,
1349+
monthlyPremiumRequests: knownPlan.monthlyPremiumRequests,
1350+
};
1351+
} else if (planId) {
1352+
// Unknown plan ID — store it with no credits so the webview still shows it
1353+
this._copilotPlanResolved = { planId, planName: planId, monthlyAiCreditsUsd: 0, monthlyPremiumRequests: null };
1354+
}
1355+
if (planInfo.ide_chat !== undefined) { this.log(` IDE chat: ${planInfo.ide_chat}`); }
1356+
if (planInfo.copilot_ide_agent !== undefined) { this.log(` Agent mode: ${planInfo.copilot_ide_agent}`); }
1357+
if (planInfo.public_code_suggestions !== undefined) { this.log(` Public code suggestions: ${planInfo.public_code_suggestions}`); }
1358+
if (planInfo.unlimited_pr_summaries !== undefined) { this.log(` Unlimited PR summaries: ${planInfo.unlimited_pr_summaries}`); }
1359+
} catch (err) {
1360+
this.warn('Failed to load Copilot plan info: ' + String(err));
1361+
}
1362+
}
1363+
13151364
public async updateTokenStats(silent: boolean = false): Promise<DetailedStats | undefined> {
13161365
try {
13171366
this.log('Updating token stats...');
@@ -6437,6 +6486,7 @@ ${hashtag}`;
64376486
backendConfigured: this.isBackendConfigured(),
64386487
sortSettings,
64396488
compactNumbers: this.getCompactNumbersSetting(),
6489+
copilotPlan: this._copilotPlanResolved,
64406490
};
64416491
const initialData = JSON.stringify(dataWithBackend).replace(
64426492
/</g,

vscode-extension/src/githubPrService.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,78 @@ export type RepoPrStatsResult = {
2626
since: string; // ISO date string
2727
};
2828

29+
// ---------------------------------------------------------------------------
30+
// Copilot plan info
31+
// ---------------------------------------------------------------------------
32+
33+
export type CopilotPlanInfo = {
34+
copilot_plan?: string; // e.g. "copilot_individual" | "copilot_business" | "copilot_enterprise" | "copilot_free"
35+
public_code_suggestions?: string; // "block" | "allow"
36+
ide_chat?: string; // "enabled" | "disabled"
37+
copilot_ide_agent?: string; // "enabled" | "disabled"
38+
unlimited_pr_summaries?: boolean;
39+
assignee?: { login?: string; id?: number };
40+
[key: string]: unknown;
41+
};
42+
43+
export type CopilotPlanResult = { planInfo?: CopilotPlanInfo; statusCode?: number; error?: string };
44+
45+
/** Internal low-level fetcher for the copilot_internal/user endpoint. */
46+
function fetchCopilotPlanInfoPage(token: string): Promise<CopilotPlanResult> {
47+
return new Promise((resolve) => {
48+
const req = https.request(
49+
{
50+
hostname: 'api.github.com',
51+
path: '/copilot_internal/user',
52+
headers: {
53+
Authorization: `Bearer ${token}`,
54+
'User-Agent': 'copilot-token-tracker',
55+
Accept: 'application/json',
56+
},
57+
},
58+
(res) => {
59+
let data = '';
60+
res.on('data', (chunk) => (data += chunk));
61+
res.on('end', () => {
62+
const statusCode = res.statusCode ?? 0;
63+
if (statusCode < 200 || statusCode >= 300) {
64+
resolve({ statusCode, error: `HTTP ${statusCode}` });
65+
return;
66+
}
67+
try {
68+
const parsed = JSON.parse(data);
69+
if (typeof parsed !== 'object' || parsed === null) {
70+
resolve({ statusCode, error: 'Unexpected response format' });
71+
return;
72+
}
73+
resolve({ planInfo: parsed as CopilotPlanInfo, statusCode });
74+
} catch (e) {
75+
resolve({ statusCode, error: String(e) });
76+
}
77+
});
78+
},
79+
);
80+
req.on('error', (e) => resolve({ error: e.message }));
81+
req.setTimeout(15000, () => {
82+
req.destroy(new Error('Request timed out after 15 s'));
83+
});
84+
req.end();
85+
});
86+
}
87+
88+
/**
89+
* Fetch GitHub Copilot plan information for the authenticated user.
90+
* Uses the VS Code-only internal endpoint `https://api.github.com/copilot_internal/user`.
91+
* Treat as best-effort — this endpoint may not be available for all accounts.
92+
* @param fetcher Injectable fetcher for testing; defaults to the real HTTPS implementation.
93+
*/
94+
export function fetchCopilotPlanInfo(
95+
token: string,
96+
fetcher: (token: string) => Promise<CopilotPlanResult> = fetchCopilotPlanInfoPage,
97+
): Promise<CopilotPlanResult> {
98+
return fetcher(token);
99+
}
100+
29101
/** Detect which AI system a GitHub login belongs to, or null if not an AI bot. */
30102
export function detectAiType(login: string): RepoPrDetail['aiType'] | null {
31103
const l = login.toLowerCase();

vscode-extension/src/webview/details/main.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ type DetailedStats = {
4040
lastUpdated: string | Date;
4141
backendConfigured?: boolean;
4242
compactNumbers?: boolean;
43+
copilotPlan?: {
44+
planId: string;
45+
planName: string;
46+
monthlyAiCreditsUsd: number;
47+
monthlyPremiumRequests: number | null;
48+
};
4349
sortSettings?: {
4450
editor?: { key?: string; dir?: string };
4551
model?: { key?: string; dir?: string };
@@ -236,6 +242,15 @@ function buildMetricsSection(
236242
icon: '🟢', color: '#7ce38b',
237243
today: formatCost(stats.today.estimatedCostCopilot ?? 0), last30Days: formatCost(stats.last30Days.estimatedCostCopilot ?? 0), lastMonth: formatCost(stats.lastMonth.estimatedCostCopilot ?? 0), projected: formatCost(projections.projectedCostCopilot ?? 0)
238244
},
245+
...(stats.copilotPlan ? (() => {
246+
const credits = stats.copilotPlan.monthlyAiCreditsUsd > 0 ? `$${stats.copilotPlan.monthlyAiCreditsUsd} credits/month` : 'no credits';
247+
return [{
248+
label: `${stats.copilotPlan.planName} (${credits})`,
249+
labelTooltip: `Your active GitHub Copilot subscription plan (ID: ${stats.copilotPlan.planId}). Included AI credits cover token-based billing (1 AI credit = $0.01).`,
250+
icon: '🏷️', color: '#60a5fa',
251+
today: '—', last30Days: '—', lastMonth: '—', projected: '—'
252+
}];
253+
})() : []),
239254
{ label: 'Sessions', icon: '📅', color: '#66aaff', today: formatNumber(stats.today.sessions), last30Days: formatNumber(stats.last30Days.sessions), lastMonth: formatNumber(stats.lastMonth.sessions), projected: formatNumber(projections.projectedSessions) },
240255
{ label: 'Average interactions/session', icon: '💬', color: '#8ce0ff', today: formatNumber(stats.today.avgInteractionsPerSession), last30Days: formatNumber(stats.last30Days.avgInteractionsPerSession), lastMonth: formatNumber(stats.lastMonth.avgInteractionsPerSession), projected: '—' },
241256
{ label: 'Average tokens/session', icon: '🔢', color: '#7ce38b', today: formatCompact(stats.today.avgTokensPerSession), last30Days: formatCompact(stats.last30Days.avgTokensPerSession), lastMonth: formatCompact(stats.lastMonth.avgTokensPerSession), projected: '—' }

vscode-extension/test/unit/githubPrService.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import test from 'node:test';
22
import * as assert from 'node:assert/strict';
3-
import { detectAiType, fetchRepoPrs } from '../../src/githubPrService';
3+
import { detectAiType, fetchRepoPrs, fetchCopilotPlanInfo, type CopilotPlanInfo } from '../../src/githubPrService';
44

55
// ---------------------------------------------------------------------------
66
// detectAiType — pure function, no I/O
@@ -145,3 +145,60 @@ test('fetchRepoPrs: propagates generic error from fetchPage', async () => {
145145
assert.equal(prs.length, 0);
146146
assert.equal(error, 'Network error');
147147
});
148+
149+
// ---------------------------------------------------------------------------
150+
// fetchCopilotPlanInfo — uses injectable fetcher
151+
// ---------------------------------------------------------------------------
152+
153+
test('fetchCopilotPlanInfo: returns plan info on success', async () => {
154+
const planData: CopilotPlanInfo = {
155+
copilot_plan: 'copilot_individual',
156+
ide_chat: 'enabled',
157+
copilot_ide_agent: 'enabled',
158+
public_code_suggestions: 'block',
159+
unlimited_pr_summaries: true,
160+
};
161+
const mockFetcher = async () => ({ planInfo: planData, statusCode: 200 });
162+
const { planInfo, statusCode, error } = await fetchCopilotPlanInfo('token', mockFetcher);
163+
assert.equal(error, undefined);
164+
assert.equal(statusCode, 200);
165+
assert.deepEqual(planInfo, planData);
166+
});
167+
168+
test('fetchCopilotPlanInfo: returns error on non-2xx response', async () => {
169+
const mockFetcher = async () => ({ statusCode: 401, error: 'HTTP 401' });
170+
const { planInfo, statusCode, error } = await fetchCopilotPlanInfo('token', mockFetcher);
171+
assert.equal(planInfo, undefined);
172+
assert.equal(statusCode, 401);
173+
assert.equal(error, 'HTTP 401');
174+
});
175+
176+
test('fetchCopilotPlanInfo: returns error on 403 response', async () => {
177+
const mockFetcher = async () => ({ statusCode: 403, error: 'HTTP 403' });
178+
const { planInfo, statusCode, error } = await fetchCopilotPlanInfo('token', mockFetcher);
179+
assert.equal(planInfo, undefined);
180+
assert.equal(statusCode, 403);
181+
});
182+
183+
test('fetchCopilotPlanInfo: returns error on network failure', async () => {
184+
const mockFetcher = async () => ({ error: 'socket hang up' });
185+
const { planInfo, error } = await fetchCopilotPlanInfo('token', mockFetcher);
186+
assert.equal(planInfo, undefined);
187+
assert.equal(error, 'socket hang up');
188+
});
189+
190+
test('fetchCopilotPlanInfo: returns error on unexpected response format', async () => {
191+
const mockFetcher = async () => ({ statusCode: 200, error: 'Unexpected response format' });
192+
const { planInfo, error } = await fetchCopilotPlanInfo('token', mockFetcher);
193+
assert.equal(planInfo, undefined);
194+
assert.ok(error?.includes('Unexpected response format'));
195+
});
196+
197+
test('fetchCopilotPlanInfo: handles partial plan data gracefully', async () => {
198+
// Not all fields may be present — only copilot_plan returned
199+
const mockFetcher = async () => ({ planInfo: { copilot_plan: 'copilot_free' } as CopilotPlanInfo, statusCode: 200 });
200+
const { planInfo, error } = await fetchCopilotPlanInfo('token', mockFetcher);
201+
assert.equal(error, undefined);
202+
assert.equal(planInfo?.copilot_plan, 'copilot_free');
203+
assert.equal(planInfo?.ide_chat, undefined);
204+
});

0 commit comments

Comments
 (0)