obsidian-e2e 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,9 @@
1
+ import { n as parseNoteDocument, t as createNoteDocument } from "./document-DunL2Moz.mjs";
1
2
  import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
3
  import path, { posix } from "node:path";
3
4
  import { spawn } from "node:child_process";
4
- import os from "node:os";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
+ import os from "node:os";
6
7
  //#region src/core/args.ts
7
8
  function buildCommandArgv(vaultName, command, args = {}) {
8
9
  const argv = [`vault=${vaultName}`, command];
@@ -94,6 +95,133 @@ function isMissingFileError(error) {
94
95
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
95
96
  }
96
97
  //#endregion
98
+ //#region src/core/errors.ts
99
+ var ObsidianCommandError = class extends Error {
100
+ result;
101
+ constructor(message, result) {
102
+ super(message);
103
+ this.name = "ObsidianCommandError";
104
+ this.result = result;
105
+ }
106
+ };
107
+ var WaitForTimeoutError = class extends Error {
108
+ causeError;
109
+ constructor(message, causeError) {
110
+ super(message);
111
+ this.name = "WaitForTimeoutError";
112
+ this.causeError = causeError;
113
+ }
114
+ };
115
+ var DevEvalError = class extends Error {
116
+ remote;
117
+ constructor(message, remote) {
118
+ super(message);
119
+ this.name = "DevEvalError";
120
+ this.remote = remote;
121
+ if (remote.stack) this.stack = `${this.name}: ${message}\nRemote stack:\n${remote.stack}`;
122
+ }
123
+ };
124
+ //#endregion
125
+ //#region src/dev/eval-json.ts
126
+ async function runEvalJson(dev, code, execOptions = {}) {
127
+ return parseEvalJsonEnvelope(await dev.evalRaw(buildEvalJsonCode(code), execOptions));
128
+ }
129
+ function buildEvalJsonCode(code) {
130
+ return [
131
+ "(()=>{",
132
+ `const __obsidianE2ECode=${JSON.stringify(code)};`,
133
+ "const __obsidianE2ESerialize=(value,path='$')=>{",
134
+ "if(value===null){return null;}",
135
+ "if(value===undefined){return {__obsidianE2EType:'undefined'};}",
136
+ "const valueType=typeof value;",
137
+ "if(valueType==='string'||valueType==='boolean'){return value;}",
138
+ "if(valueType==='number'){if(!Number.isFinite(value)){throw new Error(`Cannot serialize non-finite number at ${path}.`);}return value;}",
139
+ "if(valueType==='bigint'||valueType==='function'||valueType==='symbol'){throw new Error(`Cannot serialize ${valueType} at ${path}.`);}",
140
+ "if(Array.isArray(value)){return value.map((item,index)=>__obsidianE2ESerialize(item,`${path}[${index}]`));}",
141
+ "const prototype=Object.getPrototypeOf(value);",
142
+ "if(prototype!==Object.prototype&&prototype!==null){throw new Error(`Cannot serialize non-plain object at ${path}.`);}",
143
+ "const next={};",
144
+ "for(const [key,entry] of Object.entries(value)){next[key]=__obsidianE2ESerialize(entry,`${path}.${key}`);}",
145
+ "return next;",
146
+ "};",
147
+ "try{",
148
+ "return JSON.stringify({ok:true,value:__obsidianE2ESerialize((0,eval)(__obsidianE2ECode))});",
149
+ "}catch(error){",
150
+ "return JSON.stringify({ok:false,error:{message:error instanceof Error?error.message:String(error),name:error instanceof Error?error.name:'Error',stack:error instanceof Error?error.stack:undefined}});",
151
+ "}",
152
+ "})()"
153
+ ].join("");
154
+ }
155
+ function parseDevEvalOutput(raw) {
156
+ const normalized = normalizeEvalOutput(raw);
157
+ try {
158
+ return JSON.parse(normalized);
159
+ } catch {
160
+ return normalized;
161
+ }
162
+ }
163
+ function parseEvalJsonEnvelope(raw) {
164
+ const envelope = JSON.parse(normalizeEvalOutput(raw));
165
+ if (!envelope.ok) throw new DevEvalError(`Failed to evaluate Obsidian code: ${envelope.error.message}`, { ...envelope.error });
166
+ return decodeEvalJsonValue(envelope.value);
167
+ }
168
+ function normalizeEvalOutput(raw) {
169
+ return raw.startsWith("=> ") ? raw.slice(3) : raw;
170
+ }
171
+ function decodeEvalJsonValue(value) {
172
+ if (Array.isArray(value)) return value.map((entry) => decodeEvalJsonValue(entry));
173
+ if (!value || typeof value !== "object") return value;
174
+ if (isUndefinedSentinel(value)) return;
175
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeEvalJsonValue(entry)]));
176
+ }
177
+ function isUndefinedSentinel(value) {
178
+ return "__obsidianE2EType" in value && value.__obsidianE2EType === "undefined";
179
+ }
180
+ //#endregion
181
+ //#region src/metadata/metadata.ts
182
+ function createObsidianMetadataHandle(client) {
183
+ return {
184
+ async fileCache(path, execOptions) {
185
+ return readMetadata(client, "metadata", path, execOptions);
186
+ },
187
+ async frontmatter(path, execOptions) {
188
+ return readMetadata(client, "frontmatter", path, execOptions);
189
+ },
190
+ async waitForFileCache(path, predicate, options) {
191
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata cache", options);
192
+ },
193
+ async waitForFrontmatter(path, predicate, options) {
194
+ return waitForPresentValue(client, path, () => client.metadata.frontmatter(path), predicate, "frontmatter", options);
195
+ },
196
+ async waitForMetadata(path, predicate, options) {
197
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata", options);
198
+ }
199
+ };
200
+ }
201
+ async function readMetadata(client, method, path, execOptions) {
202
+ return runEvalJson(client.dev, buildMetadataReadCode(method, path), execOptions);
203
+ }
204
+ function buildMetadataReadCode(method, path) {
205
+ return [
206
+ "(()=>{",
207
+ `const __obsidianE2EPath=${JSON.stringify(path)};`,
208
+ "const __obsidianE2EFile=app?.vault?.getAbstractFileByPath?.(__obsidianE2EPath);",
209
+ "if(!__obsidianE2EFile){return null;}",
210
+ method === "frontmatter" ? "return app?.metadataCache?.getFileCache?.(__obsidianE2EFile)?.frontmatter ?? null;" : "return app?.metadataCache?.getFileCache?.(__obsidianE2EFile) ?? null;",
211
+ "})()"
212
+ ].join("");
213
+ }
214
+ async function waitForPresentValue(client, path, readValue, predicate, label, options = {}) {
215
+ return client.waitFor(async () => {
216
+ const value = await readValue();
217
+ if (value === null) return false;
218
+ return await (predicate?.(value) ?? true) ? value : false;
219
+ }, {
220
+ ...options,
221
+ message: options.message ?? `vault path "${path}" to expose ${label}`
222
+ });
223
+ }
224
+ //#endregion
97
225
  //#region src/vault/json-file.ts
98
226
  function createJsonFile(filePath, beforeMutate) {
99
227
  return {
@@ -125,17 +253,17 @@ function createPluginHandle(client, id) {
125
253
  }
126
254
  async function isLoadedInApp() {
127
255
  try {
128
- return await client.dev.eval(`(() => {
129
- const plugins = app?.plugins;
130
- return Boolean(
131
- plugins?.enabledPlugins?.has?.(${JSON.stringify(id)}) &&
132
- plugins?.plugins?.[${JSON.stringify(id)}],
133
- );
134
- })()`);
256
+ return await runEvalJson(client.dev, buildPluginLoadedCode(id));
135
257
  } catch {
136
258
  return false;
137
259
  }
138
260
  }
261
+ function withDefaultReadyReloadOptions(options = {}) {
262
+ return {
263
+ ...options,
264
+ waitUntilReady: options.waitUntilReady ?? true
265
+ };
266
+ }
139
267
  return {
140
268
  data() {
141
269
  return {
@@ -180,6 +308,37 @@ function createPluginHandle(client, id) {
180
308
  async restoreData() {
181
309
  await getClientInternals(client).restoreFile(await resolveDataPath());
182
310
  },
311
+ async updateDataAndReload(updater, options = {}) {
312
+ const nextData = await this.data().patch(updater);
313
+ if (await this.isEnabled()) await this.reload(withDefaultReadyReloadOptions(options));
314
+ return nextData;
315
+ },
316
+ async withPatchedData(updater, run, options = {}) {
317
+ const pluginWasEnabled = await this.isEnabled();
318
+ const reloadOptions = withDefaultReadyReloadOptions(options);
319
+ let hasPatchedData = false;
320
+ let runResult;
321
+ let runError;
322
+ let restoreError;
323
+ try {
324
+ await this.data().patch(updater);
325
+ hasPatchedData = true;
326
+ if (pluginWasEnabled) await this.reload(reloadOptions);
327
+ runResult = await run(this);
328
+ } catch (error) {
329
+ runError = error;
330
+ }
331
+ if (hasPatchedData) try {
332
+ await this.restoreData();
333
+ if (pluginWasEnabled) await this.reload(reloadOptions);
334
+ } catch (error) {
335
+ restoreError = error;
336
+ }
337
+ if (runError && restoreError) throw new AggregateError([runError, restoreError], `Plugin "${id}" patch execution and restore both failed.`);
338
+ if (runError) throw runError;
339
+ if (restoreError) throw restoreError;
340
+ return runResult;
341
+ },
183
342
  async waitForData(predicate, options = {}) {
184
343
  return client.waitFor(async () => {
185
344
  try {
@@ -202,24 +361,14 @@ function createPluginHandle(client, id) {
202
361
  }
203
362
  };
204
363
  }
205
- //#endregion
206
- //#region src/core/errors.ts
207
- var ObsidianCommandError = class extends Error {
208
- result;
209
- constructor(message, result) {
210
- super(message);
211
- this.name = "ObsidianCommandError";
212
- this.result = result;
213
- }
214
- };
215
- var WaitForTimeoutError = class extends Error {
216
- causeError;
217
- constructor(message, causeError) {
218
- super(message);
219
- this.name = "WaitForTimeoutError";
220
- this.causeError = causeError;
221
- }
222
- };
364
+ function buildPluginLoadedCode(id) {
365
+ return [
366
+ "(()=>{",
367
+ "const __obsidianE2EPlugins=app?.plugins;",
368
+ `return Boolean(__obsidianE2EPlugins?.enabledPlugins?.has?.(${JSON.stringify(id)})&&__obsidianE2EPlugins?.plugins?.[${JSON.stringify(id)}]);`,
369
+ "})()"
370
+ ].join("");
371
+ }
223
372
  //#endregion
224
373
  //#region src/core/transport.ts
225
374
  const DEFAULT_TIMEOUT_MS$2 = 3e4;
@@ -266,6 +415,39 @@ const executeCommand = async ({ allowNonZeroExit = false, argv, bin, cwd, env, t
266
415
  return result;
267
416
  };
268
417
  //#endregion
418
+ //#region src/dev/diagnostics.ts
419
+ const DIAGNOSTICS_NAMESPACE = "__obsidianE2EDiagnostics";
420
+ function buildDiagnosticsCode(method) {
421
+ return [
422
+ "(()=>{",
423
+ `const __obsidianE2EMethod=${JSON.stringify(method)};`,
424
+ `const __obsidianE2ENamespace=${JSON.stringify(DIAGNOSTICS_NAMESPACE)};`,
425
+ "const __obsidianE2EMaxEntries=100;",
426
+ "const __obsidianE2EPush=(entries,value)=>{if(entries.length>=__obsidianE2EMaxEntries){entries.shift();}entries.push(value);};",
427
+ "const __obsidianE2EFormat=(value)=>{if(typeof value==='string'){return value;}try{return JSON.stringify(value);}catch{return String(value);}};",
428
+ "const __obsidianE2EClone=(value)=>{try{return JSON.parse(JSON.stringify(value));}catch{return __obsidianE2EFormat(value);}};",
429
+ "const __obsidianE2EPushRuntimeError=(source,errorLike,state)=>{const message=errorLike&&typeof errorLike==='object'&&'message' in errorLike?String(errorLike.message):String(errorLike);const stack=errorLike&&typeof errorLike==='object'&&'stack' in errorLike?String(errorLike.stack):undefined;__obsidianE2EPush(state.runtimeErrors,{at:Date.now(),message,source,stack});};",
430
+ "const root=globalThis;",
431
+ "const state=root[__obsidianE2ENamespace]??(root[__obsidianE2ENamespace]={consoleMessages:[],notices:[],runtimeErrors:[],consolePatched:false,noticePatched:false,runtimePatched:false});",
432
+ "if(!state.consolePatched&&root.console){for(const level of ['debug','error','info','log','warn']){const original=root.console?.[level];if(typeof original!=='function'){continue;}root.console[level]=(...args)=>{__obsidianE2EPush(state.consoleMessages,{args:args.map(__obsidianE2EClone),at:Date.now(),level,text:args.map(__obsidianE2EFormat).join(' ')});return original.apply(root.console,args);};}state.consolePatched=true;}",
433
+ "if(!state.runtimePatched&&typeof root.addEventListener==='function'){root.addEventListener('error',(event)=>{__obsidianE2EPushRuntimeError('error',event?.error??event?.message??'Unknown error',state);});root.addEventListener('unhandledrejection',(event)=>{__obsidianE2EPushRuntimeError('unhandledrejection',event?.reason??'Unhandled rejection',state);});state.runtimePatched=true;}",
434
+ "if(!state.noticePatched&&typeof root.Notice==='function'){const OriginalNotice=root.Notice;root.Notice=new Proxy(OriginalNotice,{construct(target,ctorArgs,newTarget){__obsidianE2EPush(state.notices,{at:Date.now(),message:__obsidianE2EFormat(ctorArgs[0]??''),timeout:typeof ctorArgs[1]==='number'&&Number.isFinite(ctorArgs[1])?ctorArgs[1]:undefined});return Reflect.construct(target,ctorArgs,newTarget);}});state.noticePatched=true;}",
435
+ "if(__obsidianE2EMethod==='reset'){state.consoleMessages.splice(0);state.notices.splice(0);state.runtimeErrors.splice(0);return true;}",
436
+ "if(__obsidianE2EMethod==='consoleMessages'){return state.consoleMessages;}",
437
+ "if(__obsidianE2EMethod==='notices'){return state.notices;}",
438
+ "if(__obsidianE2EMethod==='runtimeErrors'){return state.runtimeErrors;}",
439
+ "return {consoleMessages:state.consoleMessages,notices:state.notices,runtimeErrors:state.runtimeErrors};",
440
+ "})()"
441
+ ].join("");
442
+ }
443
+ function createDevDiagnostics(value) {
444
+ return {
445
+ consoleMessages: value?.consoleMessages ?? [],
446
+ notices: value?.notices ?? [],
447
+ runtimeErrors: value?.runtimeErrors ?? []
448
+ };
449
+ }
450
+ //#endregion
269
451
  //#region src/core/wait.ts
270
452
  const DEFAULT_INTERVAL_MS = 100;
271
453
  const DEFAULT_TIMEOUT_MS$1 = 5e3;
@@ -303,6 +485,7 @@ function createObsidianClient(options) {
303
485
  });
304
486
  let cachedVaultPath;
305
487
  const client = {};
488
+ const metadata = createObsidianMetadataHandle(client);
306
489
  const app = {
307
490
  async reload(execOptions = {}) {
308
491
  await client.exec("reload", {}, execOptions);
@@ -326,32 +509,61 @@ function createObsidianClient(options) {
326
509
  }, waitOptions);
327
510
  }
328
511
  };
512
+ const dev = {
513
+ async activeFilePath(execOptions = {}) {
514
+ return this.eval("app.workspace.getActiveFile()?.path ?? null", execOptions);
515
+ },
516
+ async consoleMessages(execOptions = {}) {
517
+ return readDiagnosticsValue(this, "consoleMessages", execOptions);
518
+ },
519
+ async diagnostics(execOptions = {}) {
520
+ return createDevDiagnostics(await readDiagnosticsValue(this, "diagnostics", execOptions));
521
+ },
522
+ async dom(options, execOptions = {}) {
523
+ const output = await client.execText("dev:dom", {
524
+ all: options.all,
525
+ attr: options.attr,
526
+ css: options.css,
527
+ inner: options.inner,
528
+ selector: options.selector,
529
+ text: options.text,
530
+ total: options.total
531
+ }, execOptions);
532
+ if (options.total) return Number.parseInt(output, 10);
533
+ if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
534
+ return output;
535
+ },
536
+ async eval(code, execOptions = {}) {
537
+ return parseDevEvalOutput(await this.evalRaw(code, execOptions));
538
+ },
539
+ async evalJson(code, execOptions = {}) {
540
+ return runEvalJson(this, code, execOptions);
541
+ },
542
+ async evalRaw(code, execOptions = {}) {
543
+ return client.execText("eval", { code }, execOptions);
544
+ },
545
+ async editorText(execOptions = {}) {
546
+ return this.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null", execOptions);
547
+ },
548
+ async notices(execOptions = {}) {
549
+ return readDiagnosticsValue(this, "notices", execOptions);
550
+ },
551
+ async resetDiagnostics(execOptions = {}) {
552
+ await readDiagnosticsValue(this, "reset", execOptions);
553
+ },
554
+ async runtimeErrors(execOptions = {}) {
555
+ return readDiagnosticsValue(this, "runtimeErrors", execOptions);
556
+ },
557
+ async screenshot(targetPath, execOptions = {}) {
558
+ await client.exec("dev:screenshot", { path: targetPath }, execOptions);
559
+ return targetPath;
560
+ }
561
+ };
329
562
  Object.assign(client, {
330
563
  app,
331
564
  bin: options.bin ?? "obsidian",
332
- dev: {
333
- async dom(options, execOptions = {}) {
334
- const output = await client.execText("dev:dom", {
335
- all: options.all,
336
- attr: options.attr,
337
- css: options.css,
338
- inner: options.inner,
339
- selector: options.selector,
340
- text: options.text,
341
- total: options.total
342
- }, execOptions);
343
- if (options.total) return Number.parseInt(output, 10);
344
- if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
345
- return output;
346
- },
347
- async eval(code, execOptions = {}) {
348
- return parseDevEvalOutput(await client.execText("eval", { code }, execOptions));
349
- },
350
- async screenshot(targetPath, execOptions = {}) {
351
- await client.exec("dev:screenshot", { path: targetPath }, execOptions);
352
- return targetPath;
353
- }
354
- },
565
+ dev,
566
+ metadata,
355
567
  command(id) {
356
568
  return {
357
569
  async exists(commandOptions = {}) {
@@ -419,6 +631,24 @@ function createObsidianClient(options) {
419
631
  await this.vaultPath();
420
632
  },
421
633
  vaultName: options.vault,
634
+ async waitForActiveFile(path, options) {
635
+ return client.waitFor(async () => {
636
+ const activePath = await dev.activeFilePath();
637
+ return activePath === path ? activePath : false;
638
+ }, {
639
+ ...options,
640
+ message: options?.message ?? `active file "${path}"`
641
+ });
642
+ },
643
+ async waitForConsoleMessage(predicate, options) {
644
+ return waitForDiagnosticEntry(client, () => client.dev.consoleMessages(), predicate, options?.message ?? "console message", options);
645
+ },
646
+ async waitForNotice(predicate, options) {
647
+ return waitForDiagnosticEntry(client, () => client.dev.notices(), typeof predicate === "string" ? (notice) => notice.message.includes(predicate) : predicate, options?.message ?? "notice", options);
648
+ },
649
+ async waitForRuntimeError(predicate, options) {
650
+ return waitForDiagnosticEntry(client, () => client.dev.runtimeErrors(), typeof predicate === "string" ? (error) => error.message.includes(predicate) : predicate, options?.message ?? "runtime error", options);
651
+ },
422
652
  waitFor(fn, waitOptions) {
423
653
  return waitForValue(fn, {
424
654
  ...waitDefaults,
@@ -435,14 +665,6 @@ function createObsidianClient(options) {
435
665
  function parseCommandIds(output) {
436
666
  return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
437
667
  }
438
- function parseDevEvalOutput(output) {
439
- const normalized = output.startsWith("=> ") ? output.slice(3) : output;
440
- try {
441
- return JSON.parse(normalized);
442
- } catch {
443
- return normalized;
444
- }
445
- }
446
668
  function parseTabs(output) {
447
669
  return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
448
670
  }
@@ -478,6 +700,19 @@ function parseWorkspace(output) {
478
700
  }
479
701
  return roots;
480
702
  }
703
+ async function waitForDiagnosticEntry(client, readEntries, predicate, label, options) {
704
+ return client.waitFor(async () => {
705
+ const entries = await readEntries();
706
+ for (const entry of entries) if (await predicate(entry)) return entry;
707
+ return false;
708
+ }, {
709
+ ...options,
710
+ message: options?.message ?? label
711
+ });
712
+ }
713
+ async function readDiagnosticsValue(dev, method, execOptions) {
714
+ return runEvalJson(dev, buildDiagnosticsCode(method), execOptions);
715
+ }
481
716
  function getWorkspaceDepth(line) {
482
717
  let depth = 0;
483
718
  let remainder = line;
@@ -518,51 +753,188 @@ function parseWorkspaceNode(line) {
518
753
  };
519
754
  }
520
755
  //#endregion
756
+ //#region src/core/path-slug.ts
757
+ function sanitizePathSegment(value, options = {}) {
758
+ const fallback = options.fallback ?? "test";
759
+ const maxLength = options.maxLength ?? 80;
760
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength) || fallback;
761
+ }
762
+ //#endregion
763
+ //#region src/vault/paths.ts
764
+ function normalizeScope(scope) {
765
+ if (!scope || scope === ".") return "";
766
+ return scope.replace(/^\/+|\/+$/g, "");
767
+ }
768
+ function resolveVaultPath(scopeRoot, targetPath) {
769
+ if (!targetPath || targetPath === ".") return scopeRoot;
770
+ return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
771
+ }
772
+ async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
773
+ const vaultPath = await obsidian.vaultPath();
774
+ const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
775
+ const resolvedPath = path.resolve(vaultPath, ...relativePath);
776
+ const normalizedVaultPath = path.resolve(vaultPath);
777
+ if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
778
+ return resolvedPath;
779
+ }
780
+ //#endregion
781
+ //#region src/vault/vault.ts
782
+ function createVaultApi(options) {
783
+ const scopeRoot = normalizeScope(options.root);
784
+ return {
785
+ async delete(targetPath, deleteOptions = {}) {
786
+ await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
787
+ force: true,
788
+ recursive: true
789
+ });
790
+ if (deleteOptions.permanent === false) return;
791
+ },
792
+ async exists(targetPath) {
793
+ try {
794
+ await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
795
+ return true;
796
+ } catch {
797
+ return false;
798
+ }
799
+ },
800
+ json(targetPath) {
801
+ const jsonFile = {
802
+ async patch(updater) {
803
+ const currentValue = await jsonFile.read();
804
+ const draft = structuredClone(currentValue);
805
+ const nextValue = await updater(draft) ?? draft;
806
+ await jsonFile.write(nextValue);
807
+ return nextValue;
808
+ },
809
+ async read() {
810
+ const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
811
+ return JSON.parse(rawValue);
812
+ },
813
+ async write(value) {
814
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
815
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
816
+ await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
817
+ }
818
+ };
819
+ return jsonFile;
820
+ },
821
+ async mkdir(targetPath) {
822
+ await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
823
+ },
824
+ async read(targetPath) {
825
+ return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
826
+ },
827
+ async waitForContent(targetPath, predicate, waitOptions = {}) {
828
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
829
+ return options.obsidian.waitFor(async () => {
830
+ try {
831
+ const content = await readFile(resolvedPath, "utf8");
832
+ return await predicate(content) ? content : false;
833
+ } catch {
834
+ return false;
835
+ }
836
+ }, {
837
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
838
+ ...waitOptions
839
+ });
840
+ },
841
+ async waitForExists(targetPath, waitOptions) {
842
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
843
+ await options.obsidian.waitFor(async () => {
844
+ try {
845
+ await access(resolvedPath);
846
+ return true;
847
+ } catch {
848
+ return false;
849
+ }
850
+ }, {
851
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
852
+ ...waitOptions
853
+ });
854
+ },
855
+ async waitForMissing(targetPath, waitOptions) {
856
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
857
+ await options.obsidian.waitFor(async () => {
858
+ try {
859
+ await access(resolvedPath);
860
+ return false;
861
+ } catch {
862
+ return true;
863
+ }
864
+ }, {
865
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
866
+ ...waitOptions
867
+ });
868
+ },
869
+ async write(targetPath, content, writeOptions = {}) {
870
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
871
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
872
+ await writeFile(resolvedPath, content, "utf8");
873
+ if (!writeOptions.waitForContent) return;
874
+ const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
875
+ await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
876
+ }
877
+ };
878
+ }
879
+ //#endregion
521
880
  //#region src/artifacts/failure-artifacts.ts
522
881
  const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
882
+ const DEFAULT_FAILURE_ARTIFACT_CAPTURE = {
883
+ activeFile: true,
884
+ activeNote: true,
885
+ consoleMessages: true,
886
+ dom: true,
887
+ editorText: true,
888
+ notices: true,
889
+ parsedFrontmatter: true,
890
+ runtimeErrors: true,
891
+ screenshot: true,
892
+ tabs: true,
893
+ workspace: true
894
+ };
523
895
  function getFailureArtifactConfig(options) {
524
896
  if (!options.captureOnFailure) return {
525
897
  artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
526
- capture: {
527
- activeFile: true,
528
- dom: true,
529
- editorText: true,
530
- screenshot: true,
531
- tabs: true,
532
- workspace: true
533
- },
898
+ capture: { ...DEFAULT_FAILURE_ARTIFACT_CAPTURE },
534
899
  enabled: false
535
900
  };
536
901
  const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
537
902
  return {
538
903
  artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
539
904
  capture: {
540
- activeFile: overrides.activeFile ?? true,
541
- dom: overrides.dom ?? true,
542
- editorText: overrides.editorText ?? true,
543
- screenshot: overrides.screenshot ?? true,
544
- tabs: overrides.tabs ?? true,
545
- workspace: overrides.workspace ?? true
905
+ ...DEFAULT_FAILURE_ARTIFACT_CAPTURE,
906
+ ...overrides
546
907
  },
547
908
  enabled: true
548
909
  };
549
910
  }
550
911
  function getFailureArtifactDirectory(artifactsDir, task) {
551
912
  const suffix = task.id.split("_").at(-1) ?? "test";
552
- return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);
913
+ return path.join(artifactsDir, `${sanitizePathSegment(task.name, { maxLength: 60 })}-${suffix}`);
553
914
  }
554
915
  async function captureFailureArtifacts(task, obsidian, options) {
555
916
  const config = getFailureArtifactConfig(options);
556
917
  if (!config.enabled) return;
557
918
  const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
558
919
  await mkdir(artifactDirectory, { recursive: true });
920
+ const activeFile = readArtifactInput(() => readActiveFilePath(obsidian));
921
+ const activeNote = readArtifactInput(async () => {
922
+ const activeFilePath = await unwrapArtifactInput(activeFile);
923
+ return activeFilePath ? readActiveNoteSnapshot(obsidian, activeFilePath) : null;
924
+ });
925
+ const diagnostics = await obsidian.dev.diagnostics().catch(() => null);
559
926
  await Promise.all([
560
- captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await obsidian.dev.eval("app.workspace.getActiveFile()?.path ?? null") })),
927
+ captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await unwrapArtifactInput(activeFile) })),
928
+ captureTextArtifact(artifactDirectory, "active-note.md", config.capture.activeNote, async () => (await unwrapArtifactInput(activeNote))?.raw ?? ""),
929
+ captureJsonArtifact(artifactDirectory, "active-note-frontmatter.json", config.capture.parsedFrontmatter, async () => ({ frontmatter: (await unwrapArtifactInput(activeNote))?.frontmatter ?? null })),
561
930
  captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
562
931
  inner: true,
563
932
  selector: ".workspace"
564
933
  }))),
565
- captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null") })),
934
+ captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.editorText() })),
935
+ captureJsonArtifact(artifactDirectory, "console-messages.json", config.capture.consoleMessages, async () => diagnostics?.consoleMessages ?? []),
936
+ captureJsonArtifact(artifactDirectory, "runtime-errors.json", config.capture.runtimeErrors, async () => diagnostics?.runtimeErrors ?? []),
937
+ captureJsonArtifact(artifactDirectory, "notices.json", config.capture.notices, async () => diagnostics?.notices ?? []),
566
938
  captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
567
939
  captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
568
940
  captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
@@ -570,14 +942,6 @@ async function captureFailureArtifacts(task, obsidian, options) {
570
942
  ]);
571
943
  return artifactDirectory;
572
944
  }
573
- async function capturePluginFailureArtifacts(task, plugin, options) {
574
- const config = getFailureArtifactConfig(options);
575
- if (!config.enabled) return;
576
- const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
577
- await mkdir(artifactDirectory, { recursive: true });
578
- await captureJsonArtifact(artifactDirectory, `${plugin.id}-data.json`, true, () => plugin.data().read());
579
- return artifactDirectory;
580
- }
581
945
  async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
582
946
  if (!enabled) return;
583
947
  try {
@@ -607,8 +971,59 @@ async function captureTextArtifact(artifactDirectory, filename, enabled, readVal
607
971
  function formatArtifactError(error) {
608
972
  return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
609
973
  }
610
- function sanitizeForPath(value) {
611
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
974
+ async function readActiveFilePath(obsidian) {
975
+ return obsidian.dev.activeFilePath();
976
+ }
977
+ async function readActiveNoteSnapshot(obsidian, activeFile) {
978
+ return parseNoteDocument(await createVaultApi({ obsidian }).read(activeFile));
979
+ }
980
+ function readArtifactInput(readValue) {
981
+ return { promise: readValue().then((value) => ({
982
+ ok: true,
983
+ value
984
+ }), (error) => ({
985
+ error,
986
+ ok: false
987
+ })) };
988
+ }
989
+ async function unwrapArtifactInput(result) {
990
+ const value = await result.promise;
991
+ if (!value.ok) throw value.error;
992
+ return value.value;
993
+ }
994
+ //#endregion
995
+ //#region src/vault/sandbox.ts
996
+ async function createSandboxApi(options) {
997
+ const root = posix.join(options.sandboxRoot, `${sanitizePathSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
998
+ const sandboxPath = (...segments) => posix.join(root, ...segments);
999
+ const vault = createVaultApi({
1000
+ obsidian: options.obsidian,
1001
+ root
1002
+ });
1003
+ await vault.mkdir(".");
1004
+ return {
1005
+ ...vault,
1006
+ async cleanup() {
1007
+ await vault.delete(".", { permanent: true });
1008
+ },
1009
+ path(...segments) {
1010
+ return sandboxPath(...segments);
1011
+ },
1012
+ async readNote(targetPath) {
1013
+ return parseNoteDocument(await vault.read(targetPath));
1014
+ },
1015
+ root,
1016
+ async writeNote(writeOptions) {
1017
+ const { path, waitForMetadata = true, waitOptions, ...noteInput } = writeOptions;
1018
+ const document = createNoteDocument(noteInput);
1019
+ await vault.write(path, document.raw);
1020
+ if (waitForMetadata) {
1021
+ const predicate = typeof waitForMetadata === "function" ? waitForMetadata : void 0;
1022
+ await options.obsidian.metadata.waitForMetadata(sandboxPath(path), predicate, waitOptions);
1023
+ }
1024
+ return document;
1025
+ }
1026
+ };
612
1027
  }
613
1028
  //#endregion
614
1029
  //#region src/fixtures/vault-lock.ts
@@ -772,147 +1187,167 @@ async function sleep(durationMs) {
772
1187
  async function writeMetadata(metadataPath, metadata) {
773
1188
  await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
774
1189
  }
775
- //#endregion
776
- //#region src/vault/paths.ts
777
- function normalizeScope(scope) {
778
- if (!scope || scope === ".") return "";
779
- return scope.replace(/^\/+|\/+$/g, "");
780
- }
781
- function resolveVaultPath(scopeRoot, targetPath) {
782
- if (!targetPath || targetPath === ".") return scopeRoot;
783
- return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
784
- }
785
- async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
786
- const vaultPath = await obsidian.vaultPath();
787
- const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
788
- const resolvedPath = path.resolve(vaultPath, ...relativePath);
789
- const normalizedVaultPath = path.resolve(vaultPath);
790
- if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
791
- return resolvedPath;
792
- }
793
- //#endregion
794
- //#region src/vault/vault.ts
795
- function createVaultApi(options) {
796
- const scopeRoot = normalizeScope(options.root);
1190
+ function createBaseFixtures(options, fixtureOptions = {}) {
1191
+ const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
797
1192
  return {
798
- async delete(targetPath, deleteOptions = {}) {
799
- await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
800
- force: true,
801
- recursive: true
1193
+ _vaultLock: [async ({}, use) => {
1194
+ if (!options.sharedVaultLock) {
1195
+ await use(null);
1196
+ return;
1197
+ }
1198
+ const lockClient = createObsidianClient(options);
1199
+ await lockClient.verify();
1200
+ const vaultLock = await acquireVaultRunLock({
1201
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1202
+ vaultName: options.vault,
1203
+ vaultPath: await lockClient.vaultPath()
802
1204
  });
803
- if (deleteOptions.permanent === false) return;
804
- },
805
- async exists(targetPath) {
1205
+ await vaultLock.publishMarker(lockClient);
806
1206
  try {
807
- await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
808
- return true;
809
- } catch {
810
- return false;
811
- }
812
- },
813
- json(targetPath) {
814
- const jsonFile = {
815
- async patch(updater) {
816
- const currentValue = await jsonFile.read();
817
- const draft = structuredClone(currentValue);
818
- const nextValue = await updater(draft) ?? draft;
819
- await jsonFile.write(nextValue);
820
- return nextValue;
821
- },
822
- async read() {
823
- const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
824
- return JSON.parse(rawValue);
825
- },
826
- async write(value) {
827
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
828
- await mkdir(path.dirname(resolvedPath), { recursive: true });
829
- await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
830
- }
831
- };
832
- return jsonFile;
833
- },
834
- async mkdir(targetPath) {
835
- await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
836
- },
837
- async read(targetPath) {
838
- return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
839
- },
840
- async waitForContent(targetPath, predicate, waitOptions = {}) {
841
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
842
- return options.obsidian.waitFor(async () => {
1207
+ await use(vaultLock);
1208
+ } finally {
843
1209
  try {
844
- const content = await readFile(resolvedPath, "utf8");
845
- return await predicate(content) ? content : false;
846
- } catch {
847
- return false;
848
- }
849
- }, {
850
- message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
851
- ...waitOptions
1210
+ await clearVaultRunLockMarker(lockClient);
1211
+ } catch {}
1212
+ await vaultLock.release();
1213
+ }
1214
+ }, { scope: "worker" }],
1215
+ _testContext: async ({ _vaultLock, onTestFailed, task }, use) => {
1216
+ let failedTask = false;
1217
+ onTestFailed(() => {
1218
+ failedTask = true;
852
1219
  });
853
- },
854
- async waitForExists(targetPath, waitOptions) {
855
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
856
- await options.obsidian.waitFor(async () => {
857
- try {
858
- await access(resolvedPath);
859
- return true;
860
- } catch {
861
- return false;
862
- }
863
- }, {
864
- message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
865
- ...waitOptions
1220
+ const context = await createInternalTestContext({
1221
+ ...options,
1222
+ createVault,
1223
+ testName: task.name,
1224
+ vaultLock: _vaultLock
866
1225
  });
1226
+ try {
1227
+ await use(context);
1228
+ } finally {
1229
+ await context.cleanup({ failedTask: failedTask ? task : void 0 });
1230
+ }
867
1231
  },
868
- async waitForMissing(targetPath, waitOptions) {
869
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
870
- await options.obsidian.waitFor(async () => {
871
- try {
872
- await access(resolvedPath);
873
- return false;
874
- } catch {
875
- return true;
876
- }
877
- }, {
878
- message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
879
- ...waitOptions
880
- });
1232
+ obsidian: async ({ _testContext }, use) => {
1233
+ await use(_testContext.obsidian);
881
1234
  },
882
- async write(targetPath, content, writeOptions = {}) {
883
- const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
884
- await mkdir(path.dirname(resolvedPath), { recursive: true });
885
- await writeFile(resolvedPath, content, "utf8");
886
- if (!writeOptions.waitForContent) return;
887
- const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
888
- await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
1235
+ sandbox: async ({ _testContext }, use) => {
1236
+ await use(_testContext.sandbox);
1237
+ },
1238
+ vault: async ({ _testContext }, use) => {
1239
+ await use(_testContext.vault);
889
1240
  }
890
1241
  };
891
1242
  }
892
1243
  //#endregion
893
- //#region src/vault/sandbox.ts
894
- async function createSandboxApi(options) {
895
- const root = posix.join(options.sandboxRoot, `${sanitizeSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
896
- const vault = createVaultApi({
897
- obsidian: options.obsidian,
898
- root
899
- });
900
- await vault.mkdir(".");
901
- return {
902
- ...vault,
903
- async cleanup() {
904
- await vault.delete(".", { permanent: true });
905
- },
906
- path(...segments) {
907
- return posix.join(root, ...segments);
908
- },
909
- root
910
- };
1244
+ //#region src/fixtures/test-context.ts
1245
+ async function createTestContext(options) {
1246
+ return createInternalTestContext(options);
1247
+ }
1248
+ async function withVaultSandbox(options, run) {
1249
+ const context = await createInternalTestContext(options);
1250
+ try {
1251
+ return await run(context);
1252
+ } finally {
1253
+ await context.cleanup();
1254
+ }
1255
+ }
1256
+ async function createInternalTestContext(options) {
1257
+ const obsidian = createObsidianClient(options);
1258
+ const trackedPlugins = /* @__PURE__ */ new Map();
1259
+ let sandbox = null;
1260
+ const vaultLock = options.vaultLock ?? await maybeAcquireVaultLock(options, obsidian);
1261
+ const ownsVaultLock = options.vaultLock === void 0 && vaultLock !== null;
1262
+ const vaultFactory = options.createVault ?? ((client) => createVaultApi({ obsidian: client }));
1263
+ let disposed = false;
1264
+ try {
1265
+ await obsidian.verify();
1266
+ if (vaultLock) await vaultLock.publishMarker(obsidian);
1267
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1268
+ const vault = await vaultFactory(obsidian);
1269
+ sandbox = await createSandboxApi({
1270
+ obsidian,
1271
+ sandboxRoot: options.sandboxRoot ?? "__obsidian_e2e__",
1272
+ testName: options.testName ?? "test"
1273
+ });
1274
+ const captureArtifacts = async (task) => captureFailureArtifacts(task, obsidian, {
1275
+ ...options,
1276
+ plugin: trackedPlugins.size === 1 ? [...trackedPlugins.values()][0].plugin : void 0
1277
+ });
1278
+ const cleanup = async (cleanupOptions = {}) => {
1279
+ if (disposed) return;
1280
+ disposed = true;
1281
+ const cleanupErrors = [];
1282
+ try {
1283
+ if (cleanupOptions.failedTask && options.captureOnFailure) await captureArtifacts(cleanupOptions.failedTask);
1284
+ } finally {
1285
+ try {
1286
+ await getClientInternals(obsidian).restoreAll();
1287
+ } finally {
1288
+ for (const session of [...trackedPlugins.values()].reverse()) if (!session.wasEnabled) await recordCleanupError(cleanupErrors, async () => session.plugin.disable({ filter: session.filter }));
1289
+ try {
1290
+ await clearVaultRunLockMarker(obsidian);
1291
+ } catch {}
1292
+ if (ownsVaultLock) await recordCleanupError(cleanupErrors, async () => vaultLock.release());
1293
+ await recordCleanupError(cleanupErrors, async () => sandbox.cleanup());
1294
+ }
1295
+ }
1296
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
1297
+ if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "One or more test cleanup steps failed.");
1298
+ };
1299
+ return {
1300
+ obsidian,
1301
+ sandbox,
1302
+ vault,
1303
+ captureFailureArtifacts: captureArtifacts,
1304
+ cleanup,
1305
+ async plugin(id, sessionOptions = {}) {
1306
+ const existing = trackedPlugins.get(id);
1307
+ if (existing) return existing.plugin;
1308
+ const plugin = obsidian.plugin(id);
1309
+ const wasEnabled = await plugin.isEnabled();
1310
+ if (!wasEnabled) await plugin.enable({ filter: sessionOptions.filter });
1311
+ if (sessionOptions.seedData !== void 0) await plugin.data().write(sessionOptions.seedData);
1312
+ trackedPlugins.set(id, {
1313
+ filter: sessionOptions.filter,
1314
+ plugin,
1315
+ wasEnabled
1316
+ });
1317
+ return plugin;
1318
+ },
1319
+ async resetDiagnostics() {
1320
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1321
+ }
1322
+ };
1323
+ } catch (error) {
1324
+ try {
1325
+ if (sandbox) await sandbox.cleanup();
1326
+ } finally {
1327
+ try {
1328
+ await clearVaultRunLockMarker(obsidian);
1329
+ } catch {}
1330
+ if (ownsVaultLock) await vaultLock.release();
1331
+ }
1332
+ throw error;
1333
+ }
911
1334
  }
912
- function sanitizeSegment(value) {
913
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "test";
1335
+ async function recordCleanupError(cleanupErrors, run) {
1336
+ try {
1337
+ await run();
1338
+ } catch (error) {
1339
+ cleanupErrors.push(error);
1340
+ }
1341
+ }
1342
+ async function maybeAcquireVaultLock(options, obsidian) {
1343
+ if (!options.sharedVaultLock) return null;
1344
+ return acquireVaultRunLock({
1345
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1346
+ vaultName: options.vault,
1347
+ vaultPath: await obsidian.vaultPath()
1348
+ });
914
1349
  }
915
1350
  //#endregion
916
- export { clearVaultRunLockMarker as a, DEFAULT_FAILURE_ARTIFACTS_DIR as c, createObsidianClient as d, getClientInternals as f, acquireVaultRunLock as i, captureFailureArtifacts as l, createVaultApi as n, inspectVaultRunLock as o, resolveFilesystemPath as r, readVaultRunLockMarker as s, createSandboxApi as t, capturePluginFailureArtifacts as u };
1351
+ export { clearVaultRunLockMarker as a, createSandboxApi as c, createVaultApi as d, resolveFilesystemPath as f, acquireVaultRunLock as i, DEFAULT_FAILURE_ARTIFACTS_DIR as l, getClientInternals as m, withVaultSandbox as n, inspectVaultRunLock as o, createObsidianClient as p, createBaseFixtures as r, readVaultRunLockMarker as s, createTestContext as t, captureFailureArtifacts as u };
917
1352
 
918
- //# sourceMappingURL=sandbox--mUbNsh7.mjs.map
1353
+ //# sourceMappingURL=test-context-Bl-e-83H.mjs.map