Path: blob/main/extensions/copilot/src/shared-fetch-utils/common/test/fetchedValue.spec.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { beforeEach, describe, expect, it } from 'vitest';6import { FetchedValue, FetchedValueOptions } from '../fetchedValue';7import { FetchBlockedError } from '../fetchTypes';89interface TestToken {10value: string;11expiresAt: number;12}1314describe('FetchedValue', () => {15let fetchCount: number;16let nextToken: TestToken;17let fetchedValue: FetchedValue<TestToken>;1819function createFetchedValue(overrides?: Partial<FetchedValueOptions<TestToken>>): FetchedValue<TestToken> {20return new FetchedValue({21fetch: async () => {22fetchCount++;23return nextToken;24},25isStale: token => token.expiresAt < Date.now(),26...overrides,27});28}2930beforeEach(() => {31fetchCount = 0;32nextToken = { value: 'token-1', expiresAt: Date.now() + 60_000 };33fetchedValue = createFetchedValue();34});3536it('value is undefined before first resolve', () => {37expect(fetchedValue.value).toBeUndefined();38});3940it('resolve fetches and caches the value', async () => {41const result = await fetchedValue.resolve();42expect(result).toBe(nextToken);43expect(fetchedValue.value).toBe(nextToken);44expect(fetchCount).toBe(1);45});4647it('resolve returns cached value when not stale', async () => {48await fetchedValue.resolve();49nextToken = { value: 'token-2', expiresAt: Date.now() + 60_000 };50const result = await fetchedValue.resolve();51expect(result.value).toBe('token-1');52expect(fetchCount).toBe(1);53});5455it('resolve re-fetches when value is stale', async () => {56nextToken = { value: 'token-1', expiresAt: Date.now() - 1 };57await fetchedValue.resolve();58expect(fetchCount).toBe(1);5960nextToken = { value: 'token-2', expiresAt: Date.now() + 60_000 };61const result = await fetchedValue.resolve();62expect(result.value).toBe('token-2');63expect(fetchCount).toBe(2);64});6566it('resolve with force bypasses staleness check', async () => {67await fetchedValue.resolve();68nextToken = { value: 'token-2', expiresAt: Date.now() + 60_000 };6970const result = await fetchedValue.resolve(true);71expect(result.value).toBe('token-2');72expect(fetchCount).toBe(2);73});7475it('concurrent resolves coalesce into a single fetch', async () => {76const [a, b, c] = await Promise.all([77fetchedValue.resolve(),78fetchedValue.resolve(),79fetchedValue.resolve(),80]);81expect(fetchCount).toBe(1);82expect(a).toBe(b);83expect(b).toBe(c);84});8586it('fetch error propagates and does not cache', async () => {87const fv = createFetchedValue({88fetch: async () => { throw new Error('network failure'); },89});9091await expect(fv.resolve()).rejects.toThrow('network failure');92expect(fv.value).toBeUndefined();93});9495it('FetchBlockedError returns cached value when one exists', async () => {96let shouldBlock = false;97const fv = new FetchedValue<TestToken>({98fetch: async () => {99if (shouldBlock) {100throw new FetchBlockedError('blocked', 5000);101}102return { value: 'good-value', expiresAt: Date.now() + 60_000 };103},104isStale: () => true,105});106await fv.resolve();107expect(fv.value!.value).toBe('good-value');108109shouldBlock = true;110const result = await fv.resolve();111expect(result.value).toBe('good-value');112});113114it('FetchBlockedError propagates when no cached value exists', async () => {115const fv = new FetchedValue<TestToken>({116fetch: async () => { throw new FetchBlockedError('blocked', 5000); },117isStale: () => true,118});119120await expect(fv.resolve()).rejects.toThrow('blocked');121expect(fv.value).toBeUndefined();122});123124it('dispose prevents further resolves', async () => {125fetchedValue.dispose();126await expect(fetchedValue.resolve()).rejects.toThrow('disposed');127});128129describe('when T includes undefined', () => {130it('does not re-fetch when the fetched value is undefined', async () => {131let undefinedFetchCount = 0;132const fv = new FetchedValue<string | undefined>({133fetch: async () => {134undefinedFetchCount++;135return undefined;136},137isStale: () => false,138});139140const first = await fv.resolve();141expect(first).toBeUndefined();142expect(undefinedFetchCount).toBe(1);143144const second = await fv.resolve();145expect(second).toBeUndefined();146expect(undefinedFetchCount).toBe(1); // should not re-fetch147});148});149});150151152