mirror of
https://github.com/cline/cline.git
synced 2026-09-06 20:41:02 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70ff4914ff |
@@ -235,6 +235,7 @@ const sharedOptions: Partial<esbuild.BuildOptions> = {
|
||||
"pino",
|
||||
"pino-roll",
|
||||
"@vscode/ripgrep", // Uses __dirname to locate the binary
|
||||
"nock", // VCR support — devDependency, dynamically imported only when CLINE_VCR is set
|
||||
],
|
||||
supported: { "top-level-await": true },
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"nock": "^15.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^4.0.17"
|
||||
|
||||
+5
-1
@@ -45,6 +45,7 @@ import { applyProviderConfig } from "./utils/provider-config"
|
||||
import { getValidCliProviders, isValidCliProvider } from "./utils/providers"
|
||||
import { findMostRecentTaskForWorkspace } from "./utils/task-history"
|
||||
import { autoUpdateOnStartup, checkForUpdates } from "./utils/update"
|
||||
import { initVcr } from "./utils/vcr"
|
||||
import { initializeCliContext } from "./vscode-context"
|
||||
import { CLI_LOG_FILE, shutdownEvent, window } from "./vscode-shim"
|
||||
|
||||
@@ -1148,7 +1149,10 @@ program
|
||||
}
|
||||
})
|
||||
|
||||
// Parse and run
|
||||
// Initialize VCR (nock-based HTTP record/playback) before parsing commands.
|
||||
// This must happen before any HTTP requests are made so nock can intercept them.
|
||||
// Does nothing if CLINE_VCR env var is not set.
|
||||
if (process.env.VITEST !== "true") {
|
||||
await initVcr(process.env.CLINE_VCR)
|
||||
program.parse()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* VCR (Video Cassette Recorder) for HTTP requests.
|
||||
*
|
||||
* Uses nock to record and replay HTTP interactions, enabling deterministic
|
||||
* testing of the CLI without making real API calls.
|
||||
*
|
||||
* Environment variables:
|
||||
* CLINE_VCR - "record" to record HTTP requests, "playback" to replay them
|
||||
* CLINE_VCR_CASSETTE - Path to the cassette file (default: ./vcr-cassette.json)
|
||||
* CLINE_VCR_FILTER - Substring to filter recorded/replayed request paths.
|
||||
* Defaults to "chat/completions" so only inference requests
|
||||
* are captured. Set to "" to record/replay all requests.
|
||||
*
|
||||
* Usage:
|
||||
* # Record only inference requests (default filter)
|
||||
* CLINE_VCR=record CLINE_VCR_CASSETTE=./fixtures/my-test.json cline task "hello"
|
||||
*
|
||||
* # Replay — auth/S3/etc. requests go through normally, only inference is mocked
|
||||
* CLINE_VCR=playback CLINE_VCR_CASSETTE=./fixtures/my-test.json cline task "hello"
|
||||
*
|
||||
* # Record everything (no filter)
|
||||
* CLINE_VCR=record CLINE_VCR_FILTER="" CLINE_VCR_CASSETTE=./fixtures/all.json cline task "hello"
|
||||
*
|
||||
* Note on net.ts / nock interception:
|
||||
* The CLI normally uses undici's fetch directly (IS_STANDALONE=true path in shared/net.ts).
|
||||
* When CLINE_VCR is set, shared/net.ts falls back to globalThis.fetch so that nock's
|
||||
* recorder/interceptors — which patch globalThis.fetch — can intercept requests.
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
import type nock from "nock"
|
||||
import type { Definition } from "nock"
|
||||
|
||||
type VcrMode = "record" | "playback"
|
||||
|
||||
// ── Response body sanitization ──────────────────────────────────────────
|
||||
// Keys whose *values* are redacted in response bodies (matched case-insensitively).
|
||||
const SENSITIVE_RESPONSE_KEYS = new Set([
|
||||
"accesskeyid", // AWS access key ID
|
||||
"secretaccesskey", // AWS secret access key
|
||||
"idtoken", // JWT / OIDC id tokens
|
||||
"refreshtoken", // Refresh tokens
|
||||
"access_token", // OAuth access tokens
|
||||
"refresh_token", // OAuth refresh tokens
|
||||
])
|
||||
|
||||
// Regex patterns that are redacted from any string value regardless of key name.
|
||||
const SENSITIVE_PATTERNS: { pattern: RegExp; replacement: string }[] = [
|
||||
// AWS access key IDs (always start with AKIA)
|
||||
{ pattern: /AKIA[A-Z0-9]{16}/g, replacement: "AKIA_REDACTED_KEY_ID" },
|
||||
]
|
||||
|
||||
/**
|
||||
* Deep-sanitize a value, redacting known sensitive keys and patterns.
|
||||
* Handles objects, arrays, plain strings, and JSON-encoded strings
|
||||
* (e.g. the remote-config `value` field which is a JSON string inside JSON).
|
||||
*/
|
||||
function sanitizeResponseValue(obj: unknown): unknown {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (typeof obj === "string") {
|
||||
// First, try to parse as embedded JSON and sanitize recursively
|
||||
try {
|
||||
const parsed = JSON.parse(obj)
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
return JSON.stringify(sanitizeResponseValue(parsed))
|
||||
}
|
||||
} catch {
|
||||
// Not JSON — fall through to pattern-based sanitization
|
||||
}
|
||||
// Apply regex patterns to plain string values
|
||||
let result = obj
|
||||
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
||||
result = result.replace(pattern, replacement)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(sanitizeResponseValue)
|
||||
}
|
||||
|
||||
if (typeof obj === "object") {
|
||||
const result: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
if (SENSITIVE_RESPONSE_KEYS.has(key.toLowerCase()) && typeof value === "string") {
|
||||
result[key] = "REDACTED"
|
||||
} else {
|
||||
result[key] = sanitizeResponseValue(value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a single recorded nock Definition, stripping sensitive data
|
||||
* from headers, request bodies, and response bodies.
|
||||
*/
|
||||
function sanitizeRecording(rec: Definition): Definition {
|
||||
const cleaned = { ...rec }
|
||||
|
||||
// Strip raw response headers (contain request IDs, dates, etc.)
|
||||
delete (cleaned as any).rawHeaders
|
||||
|
||||
// Remove sensitive request headers if present
|
||||
if (cleaned.reqheaders) {
|
||||
delete cleaned.reqheaders.authorization
|
||||
delete cleaned.reqheaders.Authorization
|
||||
delete cleaned.reqheaders["x-api-key"]
|
||||
delete cleaned.reqheaders["X-Api-Key"]
|
||||
}
|
||||
|
||||
// Remove request body (may contain prompts, API keys, etc.)
|
||||
if (cleaned.body) {
|
||||
delete cleaned.body
|
||||
}
|
||||
|
||||
// Deep-sanitize response body for embedded secrets (S3 creds, tokens, etc.)
|
||||
if (cleaned.response !== undefined) {
|
||||
cleaned.response = sanitizeResponseValue(cleaned.response) as Definition["response"]
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
interface VcrConfig {
|
||||
mode: VcrMode
|
||||
cassettePath: string
|
||||
/** Only record/replay requests whose path includes this string. "" = no filter. */
|
||||
filter: string
|
||||
}
|
||||
|
||||
function getVcrConfig(vcrMode: string | undefined): VcrConfig | null {
|
||||
if (!vcrMode) {
|
||||
return null
|
||||
}
|
||||
if (!process.env.CLINE_VCR_CASSETTE) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (vcrMode !== "record" && vcrMode !== "playback") {
|
||||
process.stderr.write(`[VCR] Invalid CLINE_VCR value: "${vcrMode}". Expected "record" or "playback".\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
const cassettePath = path.resolve(process.env.CLINE_VCR_CASSETTE)
|
||||
const filter = process.env.CLINE_VCR_FILTER ?? ""
|
||||
|
||||
return { mode: vcrMode, cassettePath, filter }
|
||||
}
|
||||
|
||||
async function importNock(): Promise<typeof nock> {
|
||||
try {
|
||||
const mod = await import("nock")
|
||||
return mod.default as typeof nock
|
||||
} catch {
|
||||
process.stderr.write(
|
||||
"[VCR] nock is required for VCR mode but is not installed.\n" +
|
||||
" Install it with: npm install -D nock\n" +
|
||||
" (nock is a devDependency in the cli package)\n",
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function startRecordingRequests(cassettePath: string, filter: string): Promise<void> {
|
||||
const nock = await importNock()
|
||||
|
||||
// Start recording — requests pass through to real servers and are captured
|
||||
nock.recorder.rec({
|
||||
output_objects: true,
|
||||
dont_print: true,
|
||||
enable_reqheaders_recording: false,
|
||||
})
|
||||
|
||||
const filterDesc = filter ? `matching path "*${filter}*"` : "all paths"
|
||||
process.stderr.write(`[VCR] Recording HTTP requests (${filterDesc}). Cassette will be saved to: ${cassettePath}\n`)
|
||||
|
||||
// Save recordings on process exit (synchronous — required by 'exit' event)
|
||||
const saveRecordings = () => {
|
||||
let recordings = nock.recorder.play() as Definition[]
|
||||
|
||||
// Filter to only matching paths if a filter is set
|
||||
if (filter) {
|
||||
recordings = recordings.filter((rec: Definition) => typeof rec.path === "string" && rec.path.includes(filter))
|
||||
}
|
||||
|
||||
if (recordings.length === 0) {
|
||||
process.stderr.write(`[VCR] No HTTP requests matching "${filter}" were recorded.\n`)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
const dir = path.dirname(cassettePath)
|
||||
fs.mkdirSync(dir, { recursive: true })
|
||||
|
||||
// Strip sensitive data from recorded interactions
|
||||
const sanitized = recordings.map(sanitizeRecording)
|
||||
|
||||
fs.writeFileSync(cassettePath, JSON.stringify(sanitized, null, 2))
|
||||
process.stderr.write(`[VCR] Saved ${sanitized.length} recorded HTTP interaction(s) to ${cassettePath}\n`)
|
||||
}
|
||||
|
||||
// The 'exit' handler fires for process.exit(), SIGINT default, etc.
|
||||
// It is synchronous-only, which is fine since we use writeFileSync.
|
||||
process.on("exit", saveRecordings)
|
||||
process.on("SIGTERM", saveRecordings)
|
||||
}
|
||||
|
||||
async function startPlayingBackRequests(cassettePath: string, filter: string): Promise<void> {
|
||||
const nock = await importNock()
|
||||
|
||||
if (!fs.existsSync(cassettePath)) {
|
||||
process.stderr.write(`[VCR] Cassette file not found: ${cassettePath}\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const recordings: Definition[] = JSON.parse(fs.readFileSync(cassettePath, "utf-8"))
|
||||
|
||||
// In filtered playback, we do NOT block all network connections.
|
||||
// Only the recorded paths are intercepted; everything else (auth, S3, etc.)
|
||||
// hits real servers so those flows work normally.
|
||||
if (!filter) {
|
||||
// No filter: block all real connections to ensure full isolation
|
||||
nock.disableNetConnect()
|
||||
nock.enableNetConnect("127.0.0.1")
|
||||
nock.enableNetConnect("localhost")
|
||||
}
|
||||
|
||||
// Define recorded interactions as nock interceptors
|
||||
const nocks = nock.define(recordings)
|
||||
|
||||
const filterDesc = filter
|
||||
? `(only paths matching "*${filter}*", all other requests go through normally)`
|
||||
: "(all requests intercepted)"
|
||||
process.stderr.write(`[VCR] Playing back ${nocks.length} recorded HTTP interaction(s) from ${cassettePath} ${filterDesc}\n`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize VCR mode based on environment variables.
|
||||
* Must be called early in the CLI startup, before HTTP requests are made.
|
||||
*
|
||||
* Does nothing if CLINE_VCR is not set.
|
||||
*/
|
||||
export async function initVcr(vcrMode: string | undefined): Promise<void> {
|
||||
const config = getVcrConfig(vcrMode)
|
||||
if (!config) {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.mode === "record") {
|
||||
await startRecordingRequests(config.cassettePath, config.filter)
|
||||
} else {
|
||||
await startPlayingBackRequests(config.cassettePath, config.filter)
|
||||
}
|
||||
}
|
||||
Generated
+86
@@ -201,6 +201,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.25.0",
|
||||
"ink-testing-library": "^4.0.0",
|
||||
"nock": "^15.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^4.0.17"
|
||||
@@ -3896,6 +3897,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@mswjs/interceptors": {
|
||||
"version": "0.39.8",
|
||||
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz",
|
||||
"integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@open-draft/deferred-promise": "^2.2.0",
|
||||
"@open-draft/logger": "^0.3.0",
|
||||
"@open-draft/until": "^2.0.0",
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.3",
|
||||
"strict-event-emitter": "^0.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -3931,6 +3950,31 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-draft/deferred-promise": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
|
||||
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@open-draft/logger": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
|
||||
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-draft/until": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
@@ -14503,6 +14547,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-node-process": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
|
||||
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -15494,6 +15545,13 @@
|
||||
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -17325,6 +17383,20 @@
|
||||
"path-to-regexp": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-15.0.0.tgz",
|
||||
"integrity": "sha512-EoAVk4Y8Yv4JUQz62sv8zmv+DoBblD/pht/q7aW/td1WietaFWrivzhMGGCLmx2qjpLcOrbyammedW8IRUU5TA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mswjs/interceptors": "^0.39.5",
|
||||
"json-stringify-safe": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20.0 <20 || >=20.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.87.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
|
||||
@@ -18359,6 +18431,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/outvariant": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
|
||||
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/own-keys": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||
@@ -21074,6 +21153,13 @@
|
||||
"text-decoder": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-event-emitter": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
|
||||
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
|
||||
+9
-5
@@ -112,18 +112,23 @@ let mockFetch: typeof globalThis.fetch | undefined
|
||||
export const fetch: typeof globalThis.fetch = (() => {
|
||||
// Note: Don't use Logger here; it may not be initialized.
|
||||
|
||||
let baseFetch: typeof globalThis.fetch = globalThis.fetch
|
||||
let baseFetch: typeof globalThis.fetch | null = null
|
||||
// Note: See esbuild.mjs, process.env.IS_STANDALONE is statically rewritten
|
||||
// to "true" or "false" (as strings) in the JetBrains/CLI build.
|
||||
// We must use explicit string comparison because "false" is truthy in JS.
|
||||
if (process.env.IS_STANDALONE === "true") {
|
||||
if (process.env.IS_STANDALONE === "true" && !process.env.CLINE_VCR) {
|
||||
// Configure undici with ProxyAgent
|
||||
// Skip when CLINE_VCR is set so nock can intercept via globalThis.fetch
|
||||
const agent = new EnvHttpProxyAgent({})
|
||||
setGlobalDispatcher(agent)
|
||||
baseFetch = undiciFetch as any as typeof globalThis.fetch
|
||||
}
|
||||
|
||||
return (input: string | URL | Request, init?: RequestInit): Promise<Response> => (mockFetch || baseFetch)(input, init)
|
||||
// When CLINE_VCR is set, baseFetch is null and we reference globalThis.fetch
|
||||
// lazily at call time. This is required because nock patches globalThis.fetch
|
||||
// AFTER module initialization, so an eagerly-captured reference would bypass nock.
|
||||
return (input: string | URL | Request, init?: RequestInit): Promise<Response> =>
|
||||
(mockFetch || baseFetch || globalThis.fetch)(input, init)
|
||||
})()
|
||||
|
||||
/**
|
||||
@@ -145,9 +150,8 @@ export function mockFetchForTesting<T>(theFetch: typeof globalThis.fetch, callba
|
||||
return result.finally(() => {
|
||||
mockFetch = originalMockFetch
|
||||
}) as typeof result
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
if (willResetSync) {
|
||||
mockFetch = originalMockFetch
|
||||
|
||||
Reference in New Issue
Block a user