index.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. /* eslint-disable import/no-extraneous-dependencies */
  2. /* eslint-disable @typescript-eslint/no-namespace */
  3. import crypto from "crypto";
  4. import { v4, v5 } from "uuid";
  5. import * as os from "node:os";
  6. import * as path from "node:path";
  7. import * as fs from "node:fs/promises";
  8. import { execSync } from "child_process";
  9. import { getCurrentRunTree, traceable } from "../../traceable.js";
  10. import { randomName } from "../../evaluation/_random_name.js";
  11. import { toBeRelativeCloseTo, toBeAbsoluteCloseTo, toBeSemanticCloseTo, } from "./matchers.js";
  12. import { evaluatorLogFeedbackPromises, testWrapperAsyncLocalStorageInstance, _logTestFeedback, syncExamplePromises, trackingEnabled, DEFAULT_TEST_CLIENT, } from "./globals.js";
  13. import { wrapExpect } from "./vendor/chain.js";
  14. import { getEnvironmentVariable, isJsDom } from "../env.js";
  15. import { STRIP_ANSI_REGEX, TEST_ID_DELIMITER, DEFAULT_TEST_TIMEOUT, UUID5_NAMESPACE, } from "./constants.js";
  16. export function logFeedback(feedback, config) {
  17. const context = testWrapperAsyncLocalStorageInstance.getStore();
  18. if (context === undefined) {
  19. throw new Error([
  20. `Could not retrieve test context. Make sure your logFeedback call is nested within a "ls.describe()" block.`,
  21. `See this page for more information: https://docs.smith.langchain.com/evaluation/how_to_guides/vitest_jest`,
  22. ].join("\n"));
  23. }
  24. if (context.currentExample === undefined) {
  25. throw new Error([
  26. `Could not retrieve current example. Make sure your logFeedback call is nested within a "ls.test()" block.`,
  27. `See this page for more information: https://docs.smith.langchain.com/evaluation/how_to_guides/vitest_jest`,
  28. ].join("\n"));
  29. }
  30. _logTestFeedback({
  31. ...config,
  32. exampleId: context.currentExample.id,
  33. feedback: feedback,
  34. context,
  35. runTree: context.testRootRunTree,
  36. client: context.client,
  37. });
  38. }
  39. export function logOutputs(output) {
  40. const context = testWrapperAsyncLocalStorageInstance.getStore();
  41. if (context === undefined) {
  42. throw new Error(`Could not retrieve test context. Make sure your logFeedback call is nested within a "ls.describe()" block.`);
  43. }
  44. if (context.currentExample === undefined ||
  45. context.setLoggedOutput === undefined) {
  46. throw new Error([
  47. `Could not retrieve current example. Make sure your logFeedback call is nested within a "ls.test()" block.`,
  48. `See this page for more information: https://docs.smith.langchain.com/evaluation/how_to_guides/vitest_jest`,
  49. ].join("\n"));
  50. }
  51. context.setLoggedOutput(output);
  52. }
  53. export function _objectHash(obj, depth = 0) {
  54. // Prevent infinite recursion
  55. if (depth > 50) {
  56. throw new Error("Object is too deep to check equality for serialization. Please use a simpler example.");
  57. }
  58. if (Array.isArray(obj)) {
  59. const arrayHash = obj.map((item) => _objectHash(item, depth + 1)).join(",");
  60. return crypto.createHash("sha256").update(arrayHash).digest("hex");
  61. }
  62. if (obj && typeof obj === "object") {
  63. const sortedHash = Object.keys(obj)
  64. .sort()
  65. .map((key) => `${key}:${_objectHash(obj[key], depth + 1)}`)
  66. .join(",");
  67. return crypto.createHash("sha256").update(sortedHash).digest("hex");
  68. }
  69. return (crypto
  70. .createHash("sha256")
  71. // Treat null and undefined as equal for serialization purposes
  72. .update(JSON.stringify(obj ?? null))
  73. .digest("hex"));
  74. }
  75. export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
  76. const { expect, test, describe, beforeAll, afterAll } = methods;
  77. async function _createProject(client, datasetId, projectConfig) {
  78. // Create the project, updating the experimentName until we find a unique one.
  79. let project;
  80. let experimentName = randomName();
  81. for (let i = 0; i < 10; i++) {
  82. try {
  83. project = await client.createProject({
  84. projectName: experimentName,
  85. ...projectConfig,
  86. referenceDatasetId: datasetId,
  87. });
  88. return project;
  89. }
  90. catch (e) {
  91. // Naming collision
  92. if (e?.name === "LangSmithConflictError") {
  93. const ent = v4().slice(0, 6);
  94. experimentName = `${experimentName}-${ent}`;
  95. }
  96. else {
  97. throw e;
  98. }
  99. }
  100. }
  101. throw new Error("Could not generate a unique experiment name within 10 attempts." +
  102. " Please try again.");
  103. }
  104. const datasetSetupInfo = new Map();
  105. function getExampleId(datasetId, inputs, outputs) {
  106. const identifier = JSON.stringify({
  107. datasetId,
  108. inputsHash: _objectHash(inputs),
  109. outputsHash: _objectHash(outputs ?? {}),
  110. });
  111. return v5(identifier, UUID5_NAMESPACE);
  112. }
  113. async function syncExample(params) {
  114. const { client, exampleId, inputs, outputs, metadata, createdAt, datasetId, } = params;
  115. let example;
  116. try {
  117. example = await client.readExample(exampleId);
  118. if (_objectHash(example.inputs) !== _objectHash(inputs) ||
  119. _objectHash(example.outputs ?? {}) !== _objectHash(outputs ?? {}) ||
  120. example.dataset_id !== datasetId) {
  121. await client.updateExample(exampleId, {
  122. inputs,
  123. outputs,
  124. metadata,
  125. dataset_id: datasetId,
  126. });
  127. }
  128. }
  129. catch (e) {
  130. if (e.message.includes("not found")) {
  131. example = await client.createExample(inputs, outputs, {
  132. exampleId,
  133. datasetId,
  134. createdAt: new Date(createdAt ?? new Date()),
  135. metadata,
  136. });
  137. }
  138. else {
  139. throw e;
  140. }
  141. }
  142. return example;
  143. }
  144. async function runDatasetSetup(context) {
  145. const { client: testClient, suiteName: datasetName, projectConfig, } = context;
  146. let storageValue;
  147. if (!trackingEnabled(context)) {
  148. storageValue = {
  149. createdAt: new Date().toISOString(),
  150. };
  151. }
  152. else {
  153. let dataset;
  154. try {
  155. dataset = await testClient.readDataset({
  156. datasetName,
  157. });
  158. }
  159. catch (e) {
  160. if (e.message.includes("not found")) {
  161. dataset = await testClient.createDataset(datasetName, {
  162. description: `Dataset for unit tests created on ${new Date().toISOString()}`,
  163. metadata: { __ls_runner: testRunnerName },
  164. });
  165. }
  166. else {
  167. throw e;
  168. }
  169. }
  170. const project = await _createProject(testClient, dataset.id, projectConfig);
  171. const datasetUrl = await testClient.getDatasetUrl({
  172. datasetId: dataset.id,
  173. });
  174. const experimentUrl = `${datasetUrl}/compare?selectedSessions=${project.id}`;
  175. console.log(`[LANGSMITH]: Experiment starting for dataset "${datasetName}"!\n[LANGSMITH]: View results at ${experimentUrl}`);
  176. storageValue = {
  177. dataset,
  178. project,
  179. client: testClient,
  180. experimentUrl,
  181. };
  182. }
  183. return storageValue;
  184. }
  185. function wrapDescribeMethod(method, methodName) {
  186. if (isJsDom()) {
  187. console.error(`[LANGSMITH]: You seem to be using a jsdom environment. This is not supported and you may experience unexpected behavior. Please set the "environment" or "testEnvironment" field in your test config file to "node".`);
  188. }
  189. return function (testSuiteName, fn, experimentConfig) {
  190. if (typeof method !== "function") {
  191. throw new Error(`"${methodName}" is not supported by your test runner.`);
  192. }
  193. if (testWrapperAsyncLocalStorageInstance.getStore() !== undefined) {
  194. throw new Error([
  195. `You seem to be nesting an ls.describe block named "${testSuiteName}" inside another ls.describe block.`,
  196. "This is not supported because each ls.describe block corresponds to a LangSmith dataset.",
  197. "To logically group tests, nest the native Jest or Vitest describe methods instead.",
  198. ].join("\n"));
  199. }
  200. const client = experimentConfig?.client ?? DEFAULT_TEST_CLIENT;
  201. const suiteName = experimentConfig?.testSuiteName ?? testSuiteName;
  202. let setupPromiseResolver;
  203. const setupPromise = new Promise((resolve) => {
  204. setupPromiseResolver = resolve;
  205. });
  206. return method(suiteName, () => {
  207. const startTime = new Date();
  208. const suiteUuid = v4();
  209. const environment = experimentConfig?.metadata?.ENVIRONMENT ??
  210. getEnvironmentVariable("ENVIRONMENT");
  211. const nodeEnv = experimentConfig?.metadata?.NODE_ENV ??
  212. getEnvironmentVariable("NODE_ENV");
  213. const langsmithEnvironment = experimentConfig?.metadata?.LANGSMITH_ENVIRONMENT ??
  214. getEnvironmentVariable("LANGSMITH_ENVIRONMENT");
  215. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  216. const suiteMetadata = {
  217. ...experimentConfig?.metadata,
  218. __ls_runner: testRunnerName,
  219. };
  220. if (environment !== undefined) {
  221. suiteMetadata.ENVIRONMENT = environment;
  222. }
  223. if (nodeEnv !== undefined) {
  224. suiteMetadata.NODE_ENV = nodeEnv;
  225. }
  226. if (langsmithEnvironment !== undefined) {
  227. suiteMetadata.LANGSMITH_ENVIRONMENT = langsmithEnvironment;
  228. }
  229. const context = {
  230. suiteUuid,
  231. suiteName,
  232. client,
  233. createdAt: new Date().toISOString(),
  234. projectConfig: {
  235. ...experimentConfig,
  236. metadata: suiteMetadata,
  237. },
  238. enableTestTracking: experimentConfig?.enableTestTracking,
  239. setupPromise,
  240. };
  241. beforeAll(async () => {
  242. const storageValue = await runDatasetSetup(context);
  243. datasetSetupInfo.set(suiteUuid, storageValue);
  244. setupPromiseResolver();
  245. });
  246. afterAll(async () => {
  247. await Promise.all([
  248. client.awaitPendingTraceBatches(),
  249. ...syncExamplePromises.values(),
  250. ...evaluatorLogFeedbackPromises.values(),
  251. ]);
  252. if (!trackingEnabled(context)) {
  253. return;
  254. }
  255. const examples = [...syncExamplePromises.values()];
  256. if (examples.length === 0) {
  257. return;
  258. }
  259. const endTime = new Date();
  260. let branch;
  261. let commit;
  262. let dirty;
  263. try {
  264. branch = execSync("git rev-parse --abbrev-ref HEAD")
  265. .toString()
  266. .trim();
  267. commit = execSync("git rev-parse HEAD").toString().trim();
  268. dirty = execSync("git status --porcelain").toString().trim() !== "";
  269. }
  270. catch {
  271. return;
  272. }
  273. if (branch === undefined || commit === undefined) {
  274. return;
  275. }
  276. try {
  277. let finalModifiedAt = examples.reduce((latestModifiedAt, example) => {
  278. if (new Date(latestModifiedAt).getTime() >
  279. new Date(example.modified_at).getTime()) {
  280. return latestModifiedAt;
  281. }
  282. else {
  283. return example.modified_at;
  284. }
  285. }, examples[0].modified_at);
  286. if (new Date(finalModifiedAt).getTime() < startTime.getTime()) {
  287. finalModifiedAt = endTime.toISOString();
  288. }
  289. const datasetInfo = datasetSetupInfo.get(suiteUuid);
  290. await client.updateProject(datasetInfo.project.id, {
  291. metadata: {
  292. ...suiteMetadata,
  293. commit,
  294. branch,
  295. dirty,
  296. },
  297. });
  298. await client.updateDatasetTag({
  299. datasetId: datasetInfo.dataset.id,
  300. asOf: finalModifiedAt,
  301. tag: `git:commit:${commit}`,
  302. });
  303. }
  304. catch (e) {
  305. console.error(e);
  306. return;
  307. }
  308. });
  309. /**
  310. * We cannot rely on setting AsyncLocalStorage in beforeAll or beforeEach,
  311. * due to https://github.com/jestjs/jest/issues/13653 and needing to use
  312. * the janky .enterWith.
  313. *
  314. * We also cannot do async setup in describe due to Jest restrictions.
  315. * However, .run without asynchronous logic works.
  316. *
  317. * We really just need a way to pass suiteUuid as global state to inner tests
  318. * that can handle concurrently running test suites. If we drop the
  319. * concurrency requirement, we can remove this hack.
  320. */
  321. void testWrapperAsyncLocalStorageInstance.run(context, fn);
  322. });
  323. };
  324. }
  325. const lsDescribe = Object.assign(wrapDescribeMethod(describe, "describe"), {
  326. only: wrapDescribeMethod(describe.only, "describe.only"),
  327. skip: wrapDescribeMethod(describe.skip, "describe.skip"),
  328. concurrent: wrapDescribeMethod(describe.concurrent, "describe.concurrent"),
  329. });
  330. function wrapTestMethod(method) {
  331. return function (name, lsParams, testFn, timeout) {
  332. // Due to https://github.com/jestjs/jest/issues/13653,
  333. // we must access the local store value here before
  334. // doing anything async.
  335. const context = testWrapperAsyncLocalStorageInstance.getStore();
  336. if (context !== undefined &&
  337. lsParams.config?.enableTestTracking !== undefined) {
  338. context.enableTestTracking = lsParams.config.enableTestTracking;
  339. }
  340. const { config, inputs, referenceOutputs, ...rest } = lsParams;
  341. const totalRuns = config?.iterations ?? 1;
  342. for (let i = 0; i < totalRuns; i += 1) {
  343. const testUuid = v4().replace(/-/g, "").slice(0, 13);
  344. // Jest will not group tests under the same "describe" group if you await the test and
  345. // total runs is greater than 1.
  346. const resultsPath = path.join(os.tmpdir(), "langsmith_test_results", `${testUuid}.json`);
  347. void method(`${name}${totalRuns > 1 ? `, run ${i}` : ""}${TEST_ID_DELIMITER}${testUuid}`, async (...args) => {
  348. // Jest will magically introspect args and pass a "done" callback if
  349. // we use a non-spread parameter. To obtain and pass Vitest test context
  350. // through into the test function, we must therefore refer to Vitest
  351. // args using this signature
  352. const jestlikeArgs = args[0];
  353. if (context === undefined) {
  354. throw new Error([
  355. `Could not retrieve test context.`,
  356. `Please make sure you have tracing enabled and you are wrapping all of your test cases in an "ls.describe()" function.`,
  357. `See this page for more information: https://docs.smith.langchain.com/evaluation/how_to_guides/vitest_jest`,
  358. ].join("\n"));
  359. }
  360. // Jest .concurrent is super buggy and doesn't wait for beforeAll to complete
  361. // before running test functions, so we need to wait for the setup promise
  362. // to resolve before we can continue.
  363. // Seee https://github.com/jestjs/jest/issues/4281
  364. await context.setupPromise;
  365. if (!datasetSetupInfo.get(context.suiteUuid)) {
  366. throw new Error("Dataset failed to initialize. Please check your LangSmith environment variables.");
  367. }
  368. const { dataset, createdAt, project, client, experimentUrl } = datasetSetupInfo.get(context.suiteUuid);
  369. const testInput = inputs;
  370. const testOutput = referenceOutputs ?? {};
  371. const testFeedback = [];
  372. const onFeedbackLogged = (feedback) => testFeedback.push(feedback);
  373. let loggedOutput;
  374. const setLoggedOutput = (value) => {
  375. if (loggedOutput !== undefined) {
  376. console.warn(`[WARN]: New "logOutputs()" call will override output set by previous "logOutputs()" call.`);
  377. }
  378. loggedOutput = value;
  379. };
  380. let exampleId;
  381. const runTestFn = async () => {
  382. let testContext = testWrapperAsyncLocalStorageInstance.getStore();
  383. if (testContext === undefined) {
  384. throw new Error("Could not identify test context. Please contact us for help.");
  385. }
  386. return testWrapperAsyncLocalStorageInstance.run({
  387. ...testContext,
  388. testRootRunTree: trackingEnabled(testContext)
  389. ? getCurrentRunTree()
  390. : undefined,
  391. }, async () => {
  392. testContext = testWrapperAsyncLocalStorageInstance.getStore();
  393. if (testContext === undefined) {
  394. throw new Error("Could not identify test context after setting test root run tree. Please contact us for help.");
  395. }
  396. try {
  397. const res = await testFn(Object.assign(typeof jestlikeArgs === "object" && jestlikeArgs != null
  398. ? jestlikeArgs
  399. : {}, {
  400. ...rest,
  401. inputs: testInput,
  402. referenceOutputs: testOutput,
  403. }));
  404. _logTestFeedback({
  405. exampleId,
  406. feedback: { key: "pass", score: true },
  407. context: testContext,
  408. runTree: testContext.testRootRunTree,
  409. client: testContext.client,
  410. });
  411. if (res != null) {
  412. if (loggedOutput !== undefined) {
  413. console.warn(`[WARN]: Returned value from test function will override output set by previous "logOutputs()" call.`);
  414. }
  415. loggedOutput =
  416. typeof res === "object"
  417. ? res
  418. : { result: res };
  419. }
  420. return loggedOutput;
  421. }
  422. catch (e) {
  423. _logTestFeedback({
  424. exampleId,
  425. feedback: { key: "pass", score: false },
  426. context: testContext,
  427. runTree: testContext.testRootRunTree,
  428. client: testContext.client,
  429. });
  430. const rawError = e;
  431. const strippedErrorMessage = e.message.replace(STRIP_ANSI_REGEX, "");
  432. const langsmithFriendlyError = new Error(strippedErrorMessage);
  433. langsmithFriendlyError.rawJestError = rawError;
  434. throw langsmithFriendlyError;
  435. }
  436. });
  437. };
  438. try {
  439. if (trackingEnabled(context)) {
  440. const missingFields = [];
  441. if (dataset === undefined) {
  442. missingFields.push("dataset");
  443. }
  444. if (project === undefined) {
  445. missingFields.push("project");
  446. }
  447. if (client === undefined) {
  448. missingFields.push("client");
  449. }
  450. if (missingFields.length > 0) {
  451. throw new Error(`Failed to initialize test tracking: Could not identify ${missingFields
  452. .map((field) => `"${field}"`)
  453. .join(", ")} while syncing to LangSmith. Please contact us for help.`);
  454. }
  455. exampleId = getExampleId(dataset.id, inputs, referenceOutputs);
  456. // TODO: Create or update the example in the background
  457. // Currently run end time has to be after example modified time
  458. // for examples to render properly, so we must modify the example
  459. // first before running the test.
  460. if (syncExamplePromises.get(exampleId) === undefined) {
  461. syncExamplePromises.set(exampleId, await syncExample({
  462. client,
  463. exampleId,
  464. datasetId: dataset.id,
  465. inputs,
  466. outputs: referenceOutputs ?? {},
  467. metadata: {},
  468. createdAt,
  469. }));
  470. }
  471. const traceableOptions = {
  472. reference_example_id: exampleId,
  473. project_name: project.name,
  474. metadata: {
  475. ...config?.metadata,
  476. },
  477. client,
  478. tracingEnabled: true,
  479. name,
  480. };
  481. // Pass inputs into traceable so tracing works correctly but
  482. // provide both to the user-defined test function
  483. const tracedFunction = traceable(async () => {
  484. return testWrapperAsyncLocalStorageInstance.run({
  485. ...context,
  486. currentExample: {
  487. inputs,
  488. outputs: referenceOutputs,
  489. id: exampleId,
  490. },
  491. setLoggedOutput,
  492. onFeedbackLogged,
  493. }, runTestFn);
  494. }, {
  495. ...traceableOptions,
  496. ...config,
  497. });
  498. try {
  499. await tracedFunction(testInput);
  500. }
  501. catch (e) {
  502. // Extract raw Jest error from LangSmith formatted one and throw
  503. if (e.rawJestError !== undefined) {
  504. throw e.rawJestError;
  505. }
  506. throw e;
  507. }
  508. }
  509. else {
  510. try {
  511. await testWrapperAsyncLocalStorageInstance.run({
  512. ...context,
  513. currentExample: {
  514. inputs: testInput,
  515. outputs: testOutput,
  516. },
  517. setLoggedOutput,
  518. onFeedbackLogged,
  519. }, runTestFn);
  520. }
  521. catch (e) {
  522. // Extract raw Jest error from LangSmith formatted one and throw
  523. if (e.rawJestError !== undefined) {
  524. throw e.rawJestError;
  525. }
  526. throw e;
  527. }
  528. }
  529. }
  530. finally {
  531. await fs.mkdir(path.dirname(resultsPath), { recursive: true });
  532. await fs.writeFile(resultsPath, JSON.stringify({
  533. inputs,
  534. referenceOutputs,
  535. outputs: loggedOutput,
  536. feedback: testFeedback,
  537. experimentUrl,
  538. }));
  539. }
  540. }, timeout ?? DEFAULT_TEST_TIMEOUT);
  541. }
  542. };
  543. }
  544. function createEachMethod(method) {
  545. function eachMethod(table, config) {
  546. const context = testWrapperAsyncLocalStorageInstance.getStore();
  547. if (context === undefined) {
  548. throw new Error([
  549. `Could not retrieve test context. Make sure your test is nested within a "ls.describe()" block.`,
  550. `See this page for more information: https://docs.smith.langchain.com/evaluation/how_to_guides/vitest_jest`,
  551. ].join("\n"));
  552. }
  553. return function (name, fn, timeout) {
  554. for (let i = 0; i < table.length; i += 1) {
  555. const example = table[i];
  556. wrapTestMethod(method)(`${name}, item ${i}`, {
  557. ...example,
  558. inputs: example.inputs,
  559. referenceOutputs: example.referenceOutputs,
  560. config,
  561. }, fn, timeout);
  562. }
  563. };
  564. }
  565. return eachMethod;
  566. }
  567. // Roughly mirrors: https://jestjs.io/docs/api#methods
  568. const concurrentMethod = Object.assign(wrapTestMethod(test.concurrent), {
  569. each: createEachMethod(test.concurrent),
  570. only: Object.assign(wrapTestMethod(test.concurrent.only), {
  571. each: createEachMethod(test.concurrent.only),
  572. }),
  573. skip: Object.assign(wrapTestMethod(test.concurrent.skip), {
  574. each: createEachMethod(test.concurrent.skip),
  575. }),
  576. });
  577. const lsTest = Object.assign(wrapTestMethod(test), {
  578. only: Object.assign(wrapTestMethod(test.only), {
  579. each: createEachMethod(test.only),
  580. }),
  581. skip: Object.assign(wrapTestMethod(test.skip), {
  582. each: createEachMethod(test.skip),
  583. }),
  584. concurrent: concurrentMethod,
  585. each: createEachMethod(test),
  586. });
  587. const wrappedExpect = wrapExpect(expect);
  588. return {
  589. test: lsTest,
  590. it: lsTest,
  591. describe: lsDescribe,
  592. expect: wrappedExpect,
  593. toBeRelativeCloseTo,
  594. toBeAbsoluteCloseTo,
  595. toBeSemanticCloseTo,
  596. };
  597. }
  598. export function isInTestContext() {
  599. const context = testWrapperAsyncLocalStorageInstance.getStore();
  600. return context !== undefined;
  601. }
  602. export { wrapEvaluator } from "./vendor/evaluatedBy.js";
  603. export * from "./types.js";