File Explorer

/proc/4/task/8/root/var/runtime/node_modules/@aws-sdk/node_modules/undici/lib/mock

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

snapshot-agent.js10.9 KB · 354 lines
'use strict' const Agent = require('../dispatcher/agent')const MockAgent = require('./mock-agent')const { SnapshotRecorder } = require('./snapshot-recorder')const WrapHandler = require('../handler/wrap-handler')const { InvalidArgumentError, UndiciError } = require('../core/errors')const { validateSnapshotMode } = require('./snapshot-utils') const kSnapshotRecorder = Symbol('kSnapshotRecorder')const kSnapshotMode = Symbol('kSnapshotMode')const kSnapshotPath = Symbol('kSnapshotPath')const kSnapshotLoaded = Symbol('kSnapshotLoaded')const kRealAgent = Symbol('kRealAgent') // Static flag to ensure warning is only emitted once per processlet warningEmitted = false class SnapshotAgent extends MockAgent {  constructor (opts = {}) {    // Emit experimental warning only once    if (!warningEmitted) {      process.emitWarning(        'SnapshotAgent is experimental and subject to change',        'ExperimentalWarning'      )      warningEmitted = true    }     const {      mode = 'record',      snapshotPath = null,      ...mockAgentOpts    } = opts     super(mockAgentOpts)     validateSnapshotMode(mode)     // Validate snapshotPath is provided when required    if ((mode === 'playback' || mode === 'update') && !snapshotPath) {      throw new InvalidArgumentError(`snapshotPath is required when mode is '${mode}'`)    }     this[kSnapshotMode] = mode    this[kSnapshotPath] = snapshotPath     this[kSnapshotRecorder] = new SnapshotRecorder({      snapshotPath: this[kSnapshotPath],      mode: this[kSnapshotMode],      maxSnapshots: opts.maxSnapshots,      autoFlush: opts.autoFlush,      flushInterval: opts.flushInterval,      matchHeaders: opts.matchHeaders,      ignoreHeaders: opts.ignoreHeaders,      excludeHeaders: opts.excludeHeaders,      matchBody: opts.matchBody,      matchQuery: opts.matchQuery,      caseSensitive: opts.caseSensitive,      shouldRecord: opts.shouldRecord,      shouldPlayback: opts.shouldPlayback,      excludeUrls: opts.excludeUrls    })    this[kSnapshotLoaded] = false     // For recording/update mode, we need a real agent to make actual requests    // For playback mode, we need a real agent if there are excluded URLs    if (this[kSnapshotMode] === 'record' || this[kSnapshotMode] === 'update' ||        (this[kSnapshotMode] === 'playback' && opts.excludeUrls && opts.excludeUrls.length > 0)) {      this[kRealAgent] = new Agent(opts)    }     // Auto-load snapshots in playback/update mode    if ((this[kSnapshotMode] === 'playback' || this[kSnapshotMode] === 'update') && this[kSnapshotPath]) {      this.loadSnapshots().catch(() => {        // Ignore load errors - file might not exist yet      })    }  }   dispatch (opts, handler) {    handler = WrapHandler.wrap(handler)    const mode = this[kSnapshotMode]     // Check if URL should be excluded (pass through without mocking/recording)    if (this[kSnapshotRecorder].isUrlExcluded(opts)) {      // Real agent is guaranteed by constructor when excludeUrls is configured      return this[kRealAgent].dispatch(opts, handler)    }     if (mode === 'playback' || mode === 'update') {      // Ensure snapshots are loaded      if (!this[kSnapshotLoaded]) {        // Need to load asynchronously, delegate to async version        return this.#asyncDispatch(opts, handler)      }       // Try to find existing snapshot (synchronous)      const snapshot = this[kSnapshotRecorder].findSnapshot(opts)       if (snapshot) {        // Use recorded response (synchronous)        return this.#replaySnapshot(snapshot, handler)      } else if (mode === 'update') {        // Make real request and record it (async required)        return this.#recordAndReplay(opts, handler)      } else {        // Playback mode but no snapshot found        const error = new UndiciError(`No snapshot found for ${opts.method || 'GET'} ${opts.path}`)        if (handler.onError) {          handler.onError(error)          return        }        throw error      }    } else if (mode === 'record') {      // Record mode - make real request and save response (async required)      return this.#recordAndReplay(opts, handler)    }  }   /**   * Async version of dispatch for when we need to load snapshots first   */  async #asyncDispatch (opts, handler) {    await this.loadSnapshots()    return this.dispatch(opts, handler)  }   /**   * Records a real request and replays the response   */  #recordAndReplay (opts, handler) {    const responseData = {      statusCode: null,      headers: {},      trailers: {},      body: []    }     const self = this // Capture 'this' context for use within nested handler callbacks     const recordingHandler = {      onRequestStart (controller, context) {        return handler.onRequestStart(controller, { ...context, history: this.history })      },       onRequestUpgrade (controller, statusCode, headers, socket) {        return handler.onRequestUpgrade(controller, statusCode, headers, socket)      },       onResponseStart (controller, statusCode, headers, statusMessage) {        responseData.statusCode = statusCode        responseData.headers = headers        return handler.onResponseStart(controller, statusCode, headers, statusMessage)      },       onResponseData (controller, chunk) {        responseData.body.push(chunk)        return handler.onResponseData(controller, chunk)      },       onResponseEnd (controller, trailers) {        responseData.trailers = trailers         // Record the interaction using captured 'self' context (fire and forget)        const responseBody = Buffer.concat(responseData.body)        self[kSnapshotRecorder].record(opts, {          statusCode: responseData.statusCode,          headers: responseData.headers,          body: responseBody,          trailers: responseData.trailers        })          .then(() => handler.onResponseEnd(controller, trailers))          .catch((error) => handler.onResponseError(controller, error))      }    }     // Use composed agent if available (includes interceptors), otherwise use real agent    const agent = this[kRealAgent]    return agent.dispatch(opts, recordingHandler)  }   /**   * Replays a recorded response   *   * @param {Object} snapshot - The recorded snapshot to replay.   * @param {Object} handler - The handler to call with the response data.   * @returns {void}   */  #replaySnapshot (snapshot, handler) {    try {      const { response } = snapshot       const controller = {        pause () { },        resume () { },        abort (reason) {          this.aborted = true          this.reason = reason        },         aborted: false,        paused: false      }       handler.onRequestStart(controller)       handler.onResponseStart(controller, response.statusCode, response.headers)       // Body is always stored as base64 string      const body = Buffer.from(response.body, 'base64')      handler.onResponseData(controller, body)       handler.onResponseEnd(controller, response.trailers)    } catch (error) {      handler.onError?.(error)    }  }   /**   * Loads snapshots from file   *   * @param {string} [filePath] - Optional file path to load snapshots from.   * @returns {Promise<void>} - Resolves when snapshots are loaded.   */  async loadSnapshots (filePath) {    await this[kSnapshotRecorder].loadSnapshots(filePath || this[kSnapshotPath])    this[kSnapshotLoaded] = true     // In playback mode, set up MockAgent interceptors for all snapshots    if (this[kSnapshotMode] === 'playback') {      this.#setupMockInterceptors()    }  }   /**   * Saves snapshots to file   *   * @param {string} [filePath] - Optional file path to save snapshots to.   * @returns {Promise<void>} - Resolves when snapshots are saved.   */  async saveSnapshots (filePath) {    return this[kSnapshotRecorder].saveSnapshots(filePath || this[kSnapshotPath])  }   /**   * Sets up MockAgent interceptors based on recorded snapshots.   *   * This method creates MockAgent interceptors for each recorded snapshot,   * allowing the SnapshotAgent to fall back to MockAgent's standard intercept   * mechanism in playback mode. Each interceptor is configured to persist   * (remain active for multiple requests) and responds with the recorded   * response data.   *   * Called automatically when loading snapshots in playback mode.   *   * @returns {void}   */  #setupMockInterceptors () {    for (const snapshot of this[kSnapshotRecorder].getSnapshots()) {      const { request, responses, response } = snapshot      const url = new URL(request.url)       const mockPool = this.get(url.origin)       // Handle both new format (responses array) and legacy format (response object)      const responseData = responses ? responses[0] : response      if (!responseData) continue       mockPool.intercept({        path: url.pathname + url.search,        method: request.method,        headers: request.headers,        body: request.body      }).reply(responseData.statusCode, responseData.body, {        headers: responseData.headers,        trailers: responseData.trailers      }).persist()    }  }   /**   * Gets the snapshot recorder   * @return {SnapshotRecorder} - The snapshot recorder instance   */  getRecorder () {    return this[kSnapshotRecorder]  }   /**   * Gets the current mode   * @return {import('./snapshot-utils').SnapshotMode} - The current snapshot mode   */  getMode () {    return this[kSnapshotMode]  }   /**   * Clears all snapshots   * @returns {void}   */  clearSnapshots () {    this[kSnapshotRecorder].clear()  }   /**   * Resets call counts for all snapshots (useful for test cleanup)   * @returns {void}   */  resetCallCounts () {    this[kSnapshotRecorder].resetCallCounts()  }   /**   * Deletes a specific snapshot by request options   * @param {import('./snapshot-recorder').SnapshotRequestOptions} requestOpts - Request options to identify the snapshot   * @return {Promise<boolean>} - Returns true if the snapshot was deleted, false if not found   */  deleteSnapshot (requestOpts) {    return this[kSnapshotRecorder].deleteSnapshot(requestOpts)  }   /**   * Gets information about a specific snapshot   * @returns {import('./snapshot-recorder').SnapshotInfo|null} - Snapshot information or null if not found   */  getSnapshotInfo (requestOpts) {    return this[kSnapshotRecorder].getSnapshotInfo(requestOpts)  }   /**   * Replaces all snapshots with new data (full replacement)   * @param {Array<{hash: string; snapshot: import('./snapshot-recorder').SnapshotEntryshotEntry}>|Record<string, import('./snapshot-recorder').SnapshotEntry>} snapshotData - New snapshot data to replace existing snapshots   * @returns {void}   */  replaceSnapshots (snapshotData) {    this[kSnapshotRecorder].replaceSnapshots(snapshotData)  }   /**   * Closes the agent, saving snapshots and cleaning up resources.   *   * @returns {Promise<void>}   */  async close () {    await this[kSnapshotRecorder].close()    await this[kRealAgent]?.close()    await super.close()  }} module.exports = SnapshotAgent