From 68561884da37cbfcb881a78e006db8688a484993 Mon Sep 17 00:00:00 2001 From: hharera Date: Mon, 6 Jul 2026 04:43:25 +0300 Subject: [PATCH] fix achievements when token reporting is disabled --- src/webview/page-achievements.test.ts | 90 +++++++++++++++++++++++++++ src/webview/page-achievements.ts | 32 ++++++++-- 2 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/webview/page-achievements.test.ts diff --git a/src/webview/page-achievements.test.ts b/src/webview/page-achievements.test.ts new file mode 100644 index 00000000..209b33b0 --- /dev/null +++ b/src/webview/page-achievements.test.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @vitest-environment jsdom + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const sharedMocks = vi.hoisted(() => ({ + rpc: vi.fn(), + vscodeState: {}, +})); + +vi.mock('./shared', () => ({ + rpc: sharedMocks.rpc, + rpcAllSettled: async ( + calls: { [K in keyof T]: Promise }, + fallbacks: { [K in keyof T]: T[K] }, + ): Promise => { + const results = await Promise.allSettled(calls); + return results.map((r, i) => r.status === 'fulfilled' ? r.value : fallbacks[i]) as unknown as T; + }, + formatNum: (n: number) => String(Math.round(n)), + COLORS: { blue: '#58a6ff', green: '#3fb950', red: '#f85149', yellow: '#d29922' }, + vscode: { + getState: () => sharedMocks.vscodeState, + setState: (state: Record) => { sharedMocks.vscodeState = state; }, + }, + PROGRESS_ALMOST: 80, + PROGRESS_STARTED: 10, +})); + +vi.mock('./svg-icons', () => ({ + SVG: new Proxy({}, { + get: (_target, prop) => ``, + }), +})); + +import { renderAchievements } from './page-achievements'; + +describe('renderAchievements', () => { + beforeEach(() => { + sharedMocks.rpc.mockReset(); + sharedMocks.vscodeState = {}; + document.body.innerHTML = '
'; + }); + + it('renders a disabled model achievement when token reporting is disabled', async () => { + sharedMocks.rpc.mockImplementation((method: string, params?: { pageSize?: number }) => { + switch (method) { + case 'getStats': + return Promise.resolve({ totalSessions: 2, totalRequests: 25, totalWorkspaces: 1 }); + case 'getCodeProduction': + return Promise.resolve({ + summary: { totalAiLoc: 120, totalUserLoc: 40 }, + byLanguage: { labels: ['TypeScript'] }, + }); + case 'getWorkLifeBalance': + return Promise.resolve({ maxStreak: 3, weekendReqs: 1, timeDistribution: { lateNight: 2 } }); + case 'getAntiPatterns': + return Promise.resolve({ totalOccurrences: 0 }); + case 'getHourlyDistribution': + return Promise.resolve({ hours: [0, 5, 1] }); + case 'getSessions': + return Promise.resolve({ + total: 2, + sessions: params?.pageSize === 100 + ? [{ sessionId: 's1', requestCount: 12, firstMessage: 'Refactor a TypeScript service' }] + : [], + }); + case 'getDailyActivity': + return Promise.resolve({ labels: ['2026-06-01'], loc: [120], values: [25], sessions: [2] }); + case 'getConsumption': + return Promise.reject(new Error('Token reporting is temporarily disabled')); + case 'getWorkflowOptimization': + return Promise.resolve({ clusters: [{ id: 'tool-1' }] }); + default: + return Promise.reject(new Error(`Unexpected RPC: ${method}`)); + } + }); + + const container = document.getElementById('root')!; + await expect(renderAchievements(container, {})).resolves.toBeUndefined(); + + expect(container.textContent).toContain('AI Engineer Roadmap'); + expect(container.textContent).toContain('Model Explorer'); + expect(container.textContent).toContain('Token reporting disabled'); + }); +}); diff --git a/src/webview/page-achievements.ts b/src/webview/page-achievements.ts index 0de562bf..11eb9082 100644 --- a/src/webview/page-achievements.ts +++ b/src/webview/page-achievements.ts @@ -42,6 +42,7 @@ interface AchievementStats { topLanguage: string; oldTechCount: number; hourlyPeak: number; + tokenReportingDisabled: boolean; /** Historical daily data for date estimation */ dailyLabels: string[]; dailyCumulativeLoc: number[]; @@ -163,7 +164,9 @@ const ACHIEVEMENTS: Achievement[] = [ id: 'model-explorer', title: 'Model Explorer', icon: SVG.microscope, tier: 'silver', xp: 20, category: 'diversity', description: 'Used 5+ different AI models', - evaluate: thresholdEval('uniqueModels', 5, (v) => `${v} / 5 models`), + evaluate: (s) => s.tokenReportingDisabled + ? { progress: 0, label: 'Token reporting disabled', unlocked: false } + : thresholdEval('uniqueModels', 5, (v) => `${v} / 5 models`)(s), }, // Mastery @@ -322,6 +325,22 @@ function buildDailyCumulative(labels: string[], loc: number[], reqs: number[], s return { dailyLabels: labels, dailyCumulativeLoc, dailyCumulativeReqs, dailyCumulativeSessions }; } +function isTokenReportingDisabledError(err: unknown): boolean { + return err instanceof Error && err.message === 'Token reporting is temporarily disabled'; +} + +async function getConsumptionForAchievements(filter: DateFilter): Promise<{ modelTotals: Record; tokenReportingDisabled: boolean }> { + try { + const consumption = await rpc<{ modelTotals: Record }>('getConsumption', filter as Record); + return { modelTotals: consumption.modelTotals, tokenReportingDisabled: false }; + } catch (err) { + if (isTokenReportingDisabledError(err)) { + return { modelTotals: {}, tokenReportingDisabled: true }; + } + throw err; + } +} + function evaluateAchievements(stats: AchievementStats): { evaluated: EvaluatedAchievement[]; unlocked: EvaluatedAchievement[]; locked: EvaluatedAchievement[] } { const achState = getAchState(); const now = Date.now(); @@ -495,11 +514,11 @@ export async function renderAchievements(container: HTMLElement, filter: DateFil { hours: [] }, ] as const); - const sessions = await rpc<{ total: number; sessions: { sessionId: string; requestCount: number; firstMessage: string }[] }>('getSessions', { page: 1, pageSize: 100, filter: filter as Record }); + const sessions = await rpc<{ total: number; sessions: { sessionId: string; requestCount: number; firstMessage: string }[] }>('getSessions', { page: 1, pageSize: 100, filter }); const dailyActivity = await rpc('getDailyActivity', filter as Record); - const allSessions = await rpc<{ total: number }>('getSessions', { page: 1, pageSize: 1, filter: filter as Record }); + const allSessions = await rpc<{ total: number }>('getSessions', { page: 1, pageSize: 1, filter }); const codeByLang = await rpc<{ byLanguage: { labels: string[] } }>('getCodeProduction', filter as Record); - const consumption = await rpc<{ modelTotals: Record }>('getConsumption', filter as Record); + const consumption = await getConsumptionForAchievements(filter); const workflows = await rpc<{ clusters: { id: string }[] }>('getWorkflowOptimization', filter as Record); const avgReqsPerSession = allSessions.total > 0 ? stats.totalRequests / allSessions.total : 0; @@ -537,6 +556,7 @@ export async function renderAchievements(container: HTMLElement, filter: DateFil topLanguage: codeByLang.byLanguage.labels[0] ?? 'Unknown', oldTechCount, hourlyPeak: Math.max(...(hourly.hours ?? [0])), + tokenReportingDisabled: consumption.tokenReportingDisabled, ...buildDailyCumulative(dailyActivity.labels, dailyActivity.loc, dailyActivity.values, dailyActivity.sessions), }; @@ -545,7 +565,7 @@ export async function renderAchievements(container: HTMLElement, filter: DateFil // Wire tab switching const tabs = container.querySelectorAll('.ach-tab'); - const list = container.querySelector('#ach-list')!; + const list = container.querySelector('#ach-list')!; for (const tab of tabs) { tab.addEventListener('click', () => { for (const t of tabs) t.classList.remove('ach-tab-active'); @@ -554,7 +574,7 @@ export async function renderAchievements(container: HTMLElement, filter: DateFil const filtered = cat === 'all' ? evaluated : evaluated.filter(a => a.category === cat); const fUnlocked = filtered.filter(a => a.result.unlocked); const fLocked = filtered.filter(a => !a.result.unlocked).sort((a, b) => b.result.progress - a.result.progress); - render(renderAchList(fUnlocked, fLocked), list as HTMLElement); + render(renderAchList(fUnlocked, fLocked), list); wireShareButtons(container); }); }