obsidian-e2e 0.4.0 → 0.5.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];
@@ -17,6 +18,303 @@ function buildCommandArgv(vaultName, command, args = {}) {
17
18
  return argv;
18
19
  }
19
20
  //#endregion
21
+ //#region src/dev/harness.ts
22
+ const HARNESS_NAMESPACE = "__obsidianE2E";
23
+ const HARNESS_VERSION = 1;
24
+ function buildHarnessCallCode(method, ...args) {
25
+ return `(() => {
26
+ const __obsidianE2EMethod = ${JSON.stringify(method)};
27
+ const __obsidianE2EArgs = ${JSON.stringify(args)};
28
+ const __obsidianE2ENamespace = ${JSON.stringify(HARNESS_NAMESPACE)};
29
+ const __obsidianE2EVersion = ${HARNESS_VERSION};
30
+ ${HARNESS_RUNTIME}
31
+ })()`;
32
+ }
33
+ function parseHarnessEnvelope(raw) {
34
+ const envelope = JSON.parse(raw.startsWith("=> ") ? raw.slice(3) : raw);
35
+ if (!envelope.ok) throw envelope.error;
36
+ return decodeHarnessValue(envelope.value);
37
+ }
38
+ function decodeHarnessValue(value) {
39
+ if (Array.isArray(value)) return value.map((entry) => decodeHarnessValue(entry));
40
+ if (!value || typeof value !== "object") return value;
41
+ if ("__obsidianE2EType" in value && value.__obsidianE2EType === "undefined") return;
42
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, decodeHarnessValue(entry)]));
43
+ }
44
+ function createDevDiagnostics(value) {
45
+ return {
46
+ consoleMessages: value?.consoleMessages ?? [],
47
+ notices: value?.notices ?? [],
48
+ runtimeErrors: value?.runtimeErrors ?? []
49
+ };
50
+ }
51
+ const HARNESS_RUNTIME = String.raw`
52
+ const __obsidianE2EMaxEntries = 100;
53
+
54
+ const __obsidianE2EPush = (entries, value) => {
55
+ if (entries.length >= __obsidianE2EMaxEntries) {
56
+ entries.shift();
57
+ }
58
+ entries.push(value);
59
+ };
60
+
61
+ const __obsidianE2EFormat = (value) => {
62
+ if (typeof value === "string") {
63
+ return value;
64
+ }
65
+
66
+ try {
67
+ return JSON.stringify(value);
68
+ } catch {
69
+ return String(value);
70
+ }
71
+ };
72
+
73
+ const __obsidianE2ESerialize = (value, path = "$") => {
74
+ if (value === null) {
75
+ return null;
76
+ }
77
+
78
+ if (value === undefined) {
79
+ return { __obsidianE2EType: "undefined" };
80
+ }
81
+
82
+ const valueType = typeof value;
83
+
84
+ if (valueType === "string" || valueType === "boolean") {
85
+ return value;
86
+ }
87
+
88
+ if (valueType === "number") {
89
+ if (!Number.isFinite(value)) {
90
+ throw new Error(\`Cannot serialize non-finite number at \${path}.\`);
91
+ }
92
+
93
+ return value;
94
+ }
95
+
96
+ if (valueType === "bigint" || valueType === "function" || valueType === "symbol") {
97
+ throw new Error(\`Cannot serialize \${valueType} at \${path}.\`);
98
+ }
99
+
100
+ if (Array.isArray(value)) {
101
+ return value.map((item, index) => __obsidianE2ESerialize(item, \`\${path}[\${index}]\`));
102
+ }
103
+
104
+ const prototype = Object.getPrototypeOf(value);
105
+
106
+ if (prototype !== Object.prototype && prototype !== null) {
107
+ throw new Error(\`Cannot serialize non-plain object at \${path}.\`);
108
+ }
109
+
110
+ const next = {};
111
+
112
+ for (const [key, entry] of Object.entries(value)) {
113
+ next[key] = __obsidianE2ESerialize(entry, \`\${path}.\${key}\`);
114
+ }
115
+
116
+ return next;
117
+ };
118
+
119
+ const __obsidianE2EClone = (value) => JSON.parse(JSON.stringify(__obsidianE2ESerialize(value)));
120
+
121
+ const __obsidianE2ECreateHarness = () => {
122
+ const state = {
123
+ consoleMessages: [],
124
+ notices: [],
125
+ runtimeErrors: [],
126
+ };
127
+
128
+ const pushConsoleMessage = (level, args) => {
129
+ __obsidianE2EPush(state.consoleMessages, {
130
+ args: args.map((entry) => {
131
+ try {
132
+ return __obsidianE2EClone(entry);
133
+ } catch {
134
+ return __obsidianE2EFormat(entry);
135
+ }
136
+ }),
137
+ at: Date.now(),
138
+ level,
139
+ text: args.map(__obsidianE2EFormat).join(" "),
140
+ });
141
+ };
142
+
143
+ const pushRuntimeError = (source, errorLike) => {
144
+ const message =
145
+ errorLike && typeof errorLike === "object" && "message" in errorLike
146
+ ? String(errorLike.message)
147
+ : String(errorLike);
148
+ const stack =
149
+ errorLike && typeof errorLike === "object" && "stack" in errorLike
150
+ ? String(errorLike.stack)
151
+ : undefined;
152
+
153
+ __obsidianE2EPush(state.runtimeErrors, {
154
+ at: Date.now(),
155
+ message,
156
+ source,
157
+ stack,
158
+ });
159
+ };
160
+
161
+ const installConsolePatch = (root) => {
162
+ if (root.__obsidianE2EConsolePatched) {
163
+ return;
164
+ }
165
+
166
+ for (const level of ["debug", "error", "info", "log", "warn"]) {
167
+ const original = root.console?.[level];
168
+
169
+ if (typeof original !== "function") {
170
+ continue;
171
+ }
172
+
173
+ root.console[level] = (...args) => {
174
+ pushConsoleMessage(level, args);
175
+ return original.apply(root.console, args);
176
+ };
177
+ }
178
+
179
+ root.__obsidianE2EConsolePatched = true;
180
+ };
181
+
182
+ const installRuntimePatch = (root) => {
183
+ if (root.__obsidianE2ERuntimePatched || typeof root.addEventListener !== "function") {
184
+ return;
185
+ }
186
+
187
+ root.addEventListener("error", (event) => {
188
+ pushRuntimeError("error", event?.error ?? event?.message ?? "Unknown error");
189
+ });
190
+ root.addEventListener("unhandledrejection", (event) => {
191
+ pushRuntimeError("unhandledrejection", event?.reason ?? "Unhandled rejection");
192
+ });
193
+
194
+ root.__obsidianE2ERuntimePatched = true;
195
+ };
196
+
197
+ const installNoticePatch = (root) => {
198
+ if (root.__obsidianE2ENoticePatched || typeof root.Notice !== "function") {
199
+ return;
200
+ }
201
+
202
+ const OriginalNotice = root.Notice;
203
+ root.Notice = new Proxy(OriginalNotice, {
204
+ construct(target, ctorArgs, newTarget) {
205
+ __obsidianE2EPush(state.notices, {
206
+ at: Date.now(),
207
+ message: __obsidianE2EFormat(ctorArgs[0] ?? ""),
208
+ timeout:
209
+ typeof ctorArgs[1] === "number" && Number.isFinite(ctorArgs[1])
210
+ ? ctorArgs[1]
211
+ : undefined,
212
+ });
213
+
214
+ return Reflect.construct(target, ctorArgs, newTarget);
215
+ },
216
+ });
217
+ root.__obsidianE2ENoticePatched = true;
218
+ };
219
+
220
+ const ensureInstalled = () => {
221
+ const root = globalThis;
222
+ installConsolePatch(root);
223
+ installRuntimePatch(root);
224
+ installNoticePatch(root);
225
+ };
226
+
227
+ const getFileCache = (vaultPath) => {
228
+ const file = app?.vault?.getAbstractFileByPath?.(vaultPath);
229
+ if (!file) {
230
+ return null;
231
+ }
232
+
233
+ return app?.metadataCache?.getFileCache?.(file) ?? null;
234
+ };
235
+
236
+ return {
237
+ activeFilePath() {
238
+ ensureInstalled();
239
+ return app?.workspace?.getActiveFile?.()?.path ?? null;
240
+ },
241
+ consoleMessages() {
242
+ ensureInstalled();
243
+ return state.consoleMessages;
244
+ },
245
+ diagnostics() {
246
+ ensureInstalled();
247
+ return state;
248
+ },
249
+ editorText() {
250
+ ensureInstalled();
251
+ return app?.workspace?.activeLeaf?.view?.editor?.getValue?.() ?? null;
252
+ },
253
+ eval(code) {
254
+ ensureInstalled();
255
+ return (0, eval)(code);
256
+ },
257
+ frontmatter(vaultPath) {
258
+ ensureInstalled();
259
+ return getFileCache(vaultPath)?.frontmatter ?? null;
260
+ },
261
+ metadata(vaultPath) {
262
+ ensureInstalled();
263
+ return getFileCache(vaultPath);
264
+ },
265
+ notices() {
266
+ ensureInstalled();
267
+ return state.notices;
268
+ },
269
+ pluginLoaded(pluginId) {
270
+ ensureInstalled();
271
+ const plugins = app?.plugins;
272
+ return Boolean(
273
+ plugins?.enabledPlugins?.has?.(pluginId) &&
274
+ plugins?.plugins?.[pluginId],
275
+ );
276
+ },
277
+ resetDiagnostics() {
278
+ state.consoleMessages.splice(0);
279
+ state.notices.splice(0);
280
+ state.runtimeErrors.splice(0);
281
+ ensureInstalled();
282
+ return true;
283
+ },
284
+ runtimeErrors() {
285
+ ensureInstalled();
286
+ return state.runtimeErrors;
287
+ },
288
+ };
289
+ };
290
+
291
+ const root = globalThis;
292
+ const current = root[__obsidianE2ENamespace];
293
+ const harness =
294
+ current && current.version === __obsidianE2EVersion
295
+ ? current
296
+ : (root[__obsidianE2ENamespace] = {
297
+ api: __obsidianE2ECreateHarness(),
298
+ version: __obsidianE2EVersion,
299
+ });
300
+
301
+ try {
302
+ const result = harness.api[__obsidianE2EMethod](...__obsidianE2EArgs);
303
+ return JSON.stringify({
304
+ ok: true,
305
+ value: __obsidianE2ESerialize(result),
306
+ });
307
+ } catch (error) {
308
+ return JSON.stringify({
309
+ error: {
310
+ message: error instanceof Error ? error.message : String(error),
311
+ name: error instanceof Error ? error.name : "Error",
312
+ stack: error instanceof Error ? error.stack : undefined,
313
+ },
314
+ ok: false,
315
+ });
316
+ }`;
317
+ //#endregion
20
318
  //#region src/core/exec-options.ts
21
319
  function mergeExecOptions(defaults, overrides) {
22
320
  if (!defaults) return overrides ? { ...overrides } : {};
@@ -94,6 +392,40 @@ function isMissingFileError(error) {
94
392
  return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
95
393
  }
96
394
  //#endregion
395
+ //#region src/metadata/metadata.ts
396
+ function createObsidianMetadataHandle(client) {
397
+ return {
398
+ async fileCache(path, execOptions) {
399
+ return readMetadata(client, "metadata", path, execOptions);
400
+ },
401
+ async frontmatter(path, execOptions) {
402
+ return readMetadata(client, "frontmatter", path, execOptions);
403
+ },
404
+ async waitForFileCache(path, predicate, options) {
405
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata cache", options);
406
+ },
407
+ async waitForFrontmatter(path, predicate, options) {
408
+ return waitForPresentValue(client, path, () => client.metadata.frontmatter(path), predicate, "frontmatter", options);
409
+ },
410
+ async waitForMetadata(path, predicate, options) {
411
+ return waitForPresentValue(client, path, () => client.metadata.fileCache(path), predicate, "metadata", options);
412
+ }
413
+ };
414
+ }
415
+ async function readMetadata(client, method, path, execOptions) {
416
+ return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode(method, path), execOptions));
417
+ }
418
+ async function waitForPresentValue(client, path, readValue, predicate, label, options = {}) {
419
+ return client.waitFor(async () => {
420
+ const value = await readValue();
421
+ if (value === null) return false;
422
+ return await (predicate?.(value) ?? true) ? value : false;
423
+ }, {
424
+ ...options,
425
+ message: options.message ?? `vault path "${path}" to expose ${label}`
426
+ });
427
+ }
428
+ //#endregion
97
429
  //#region src/vault/json-file.ts
98
430
  function createJsonFile(filePath, beforeMutate) {
99
431
  return {
@@ -125,17 +457,17 @@ function createPluginHandle(client, id) {
125
457
  }
126
458
  async function isLoadedInApp() {
127
459
  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
- })()`);
460
+ return parseHarnessEnvelope(await client.dev.evalRaw(buildHarnessCallCode("pluginLoaded", id)));
135
461
  } catch {
136
462
  return false;
137
463
  }
138
464
  }
465
+ function withDefaultReadyReloadOptions(options = {}) {
466
+ return {
467
+ ...options,
468
+ waitUntilReady: options.waitUntilReady ?? true
469
+ };
470
+ }
139
471
  return {
140
472
  data() {
141
473
  return {
@@ -180,6 +512,37 @@ function createPluginHandle(client, id) {
180
512
  async restoreData() {
181
513
  await getClientInternals(client).restoreFile(await resolveDataPath());
182
514
  },
515
+ async updateDataAndReload(updater, options = {}) {
516
+ const nextData = await this.data().patch(updater);
517
+ if (await this.isEnabled()) await this.reload(withDefaultReadyReloadOptions(options));
518
+ return nextData;
519
+ },
520
+ async withPatchedData(updater, run, options = {}) {
521
+ const pluginWasEnabled = await this.isEnabled();
522
+ const reloadOptions = withDefaultReadyReloadOptions(options);
523
+ let hasPatchedData = false;
524
+ let runResult;
525
+ let runError;
526
+ let restoreError;
527
+ try {
528
+ await this.data().patch(updater);
529
+ hasPatchedData = true;
530
+ if (pluginWasEnabled) await this.reload(reloadOptions);
531
+ runResult = await run(this);
532
+ } catch (error) {
533
+ runError = error;
534
+ }
535
+ if (hasPatchedData) try {
536
+ await this.restoreData();
537
+ if (pluginWasEnabled) await this.reload(reloadOptions);
538
+ } catch (error) {
539
+ restoreError = error;
540
+ }
541
+ if (runError && restoreError) throw new AggregateError([runError, restoreError], `Plugin "${id}" patch execution and restore both failed.`);
542
+ if (runError) throw runError;
543
+ if (restoreError) throw restoreError;
544
+ return runResult;
545
+ },
183
546
  async waitForData(predicate, options = {}) {
184
547
  return client.waitFor(async () => {
185
548
  try {
@@ -220,6 +583,15 @@ var WaitForTimeoutError = class extends Error {
220
583
  this.causeError = causeError;
221
584
  }
222
585
  };
586
+ var DevEvalError = class extends Error {
587
+ remote;
588
+ constructor(message, remote) {
589
+ super(message);
590
+ this.name = "DevEvalError";
591
+ this.remote = remote;
592
+ if (remote.stack) this.stack = `${this.name}: ${message}\nRemote stack:\n${remote.stack}`;
593
+ }
594
+ };
223
595
  //#endregion
224
596
  //#region src/core/transport.ts
225
597
  const DEFAULT_TIMEOUT_MS$2 = 3e4;
@@ -303,6 +675,7 @@ function createObsidianClient(options) {
303
675
  });
304
676
  let cachedVaultPath;
305
677
  const client = {};
678
+ const metadata = createObsidianMetadataHandle(client);
306
679
  const app = {
307
680
  async reload(execOptions = {}) {
308
681
  await client.exec("reload", {}, execOptions);
@@ -326,32 +699,67 @@ function createObsidianClient(options) {
326
699
  }, waitOptions);
327
700
  }
328
701
  };
702
+ const dev = {
703
+ async activeFilePath(execOptions = {}) {
704
+ return readHarnessValue(this, "activeFilePath", execOptions);
705
+ },
706
+ async consoleMessages(execOptions = {}) {
707
+ return readHarnessValue(this, "consoleMessages", execOptions);
708
+ },
709
+ async diagnostics(execOptions = {}) {
710
+ return createDevDiagnostics(await readHarnessValue(this, "diagnostics", execOptions));
711
+ },
712
+ async dom(options, execOptions = {}) {
713
+ const output = await client.execText("dev:dom", {
714
+ all: options.all,
715
+ attr: options.attr,
716
+ css: options.css,
717
+ inner: options.inner,
718
+ selector: options.selector,
719
+ text: options.text,
720
+ total: options.total
721
+ }, execOptions);
722
+ if (options.total) return Number.parseInt(output, 10);
723
+ if (options.all) return output ? output.split(/\r?\n/u).filter(Boolean) : [];
724
+ return output;
725
+ },
726
+ async eval(code, execOptions = {}) {
727
+ try {
728
+ return parseHarnessEnvelope(await this.evalRaw(buildHarnessCallCode("eval", code), execOptions));
729
+ } catch (error) {
730
+ if (error && typeof error === "object" && "message" in error && "name" in error && typeof error.message === "string" && typeof error.name === "string") throw new DevEvalError(`Failed to evaluate Obsidian code: ${error.message}`, {
731
+ message: error.message,
732
+ name: error.name,
733
+ stack: "stack" in error && typeof error.stack === "string" ? error.stack : void 0
734
+ });
735
+ throw error;
736
+ }
737
+ },
738
+ async evalRaw(code, execOptions = {}) {
739
+ return client.execText("eval", { code }, execOptions);
740
+ },
741
+ async editorText(execOptions = {}) {
742
+ return readHarnessValue(this, "editorText", execOptions);
743
+ },
744
+ async notices(execOptions = {}) {
745
+ return readHarnessValue(this, "notices", execOptions);
746
+ },
747
+ async resetDiagnostics(execOptions = {}) {
748
+ await readHarnessValue(this, "resetDiagnostics", execOptions);
749
+ },
750
+ async runtimeErrors(execOptions = {}) {
751
+ return readHarnessValue(this, "runtimeErrors", execOptions);
752
+ },
753
+ async screenshot(targetPath, execOptions = {}) {
754
+ await client.exec("dev:screenshot", { path: targetPath }, execOptions);
755
+ return targetPath;
756
+ }
757
+ };
329
758
  Object.assign(client, {
330
759
  app,
331
760
  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
- },
761
+ dev,
762
+ metadata,
355
763
  command(id) {
356
764
  return {
357
765
  async exists(commandOptions = {}) {
@@ -419,6 +827,24 @@ function createObsidianClient(options) {
419
827
  await this.vaultPath();
420
828
  },
421
829
  vaultName: options.vault,
830
+ async waitForActiveFile(path, options) {
831
+ return client.waitFor(async () => {
832
+ const activePath = await dev.activeFilePath();
833
+ return activePath === path ? activePath : false;
834
+ }, {
835
+ ...options,
836
+ message: options?.message ?? `active file "${path}"`
837
+ });
838
+ },
839
+ async waitForConsoleMessage(predicate, options) {
840
+ return waitForDiagnosticEntry(client, () => client.dev.consoleMessages(), predicate, options?.message ?? "console message", options);
841
+ },
842
+ async waitForNotice(predicate, options) {
843
+ return waitForDiagnosticEntry(client, () => client.dev.notices(), typeof predicate === "string" ? (notice) => notice.message.includes(predicate) : predicate, options?.message ?? "notice", options);
844
+ },
845
+ async waitForRuntimeError(predicate, options) {
846
+ return waitForDiagnosticEntry(client, () => client.dev.runtimeErrors(), typeof predicate === "string" ? (error) => error.message.includes(predicate) : predicate, options?.message ?? "runtime error", options);
847
+ },
422
848
  waitFor(fn, waitOptions) {
423
849
  return waitForValue(fn, {
424
850
  ...waitDefaults,
@@ -435,14 +861,6 @@ function createObsidianClient(options) {
435
861
  function parseCommandIds(output) {
436
862
  return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ", 1)[0]?.trim() ?? "").filter(Boolean);
437
863
  }
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
864
  function parseTabs(output) {
447
865
  return output.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(parseTabLine);
448
866
  }
@@ -478,6 +896,19 @@ function parseWorkspace(output) {
478
896
  }
479
897
  return roots;
480
898
  }
899
+ async function waitForDiagnosticEntry(client, readEntries, predicate, label, options) {
900
+ return client.waitFor(async () => {
901
+ const entries = await readEntries();
902
+ for (const entry of entries) if (await predicate(entry)) return entry;
903
+ return false;
904
+ }, {
905
+ ...options,
906
+ message: options?.message ?? label
907
+ });
908
+ }
909
+ async function readHarnessValue(dev, method, execOptions) {
910
+ return parseHarnessEnvelope(await dev.evalRaw(buildHarnessCallCode(method), execOptions));
911
+ }
481
912
  function getWorkspaceDepth(line) {
482
913
  let depth = 0;
483
914
  let remainder = line;
@@ -518,51 +949,188 @@ function parseWorkspaceNode(line) {
518
949
  };
519
950
  }
520
951
  //#endregion
952
+ //#region src/core/path-slug.ts
953
+ function sanitizePathSegment(value, options = {}) {
954
+ const fallback = options.fallback ?? "test";
955
+ const maxLength = options.maxLength ?? 80;
956
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, maxLength) || fallback;
957
+ }
958
+ //#endregion
959
+ //#region src/vault/paths.ts
960
+ function normalizeScope(scope) {
961
+ if (!scope || scope === ".") return "";
962
+ return scope.replace(/^\/+|\/+$/g, "");
963
+ }
964
+ function resolveVaultPath(scopeRoot, targetPath) {
965
+ if (!targetPath || targetPath === ".") return scopeRoot;
966
+ return scopeRoot ? posix.join(scopeRoot, targetPath) : posix.normalize(targetPath);
967
+ }
968
+ async function resolveFilesystemPath(obsidian, scopeRoot, targetPath) {
969
+ const vaultPath = await obsidian.vaultPath();
970
+ const relativePath = resolveVaultPath(scopeRoot, targetPath).split("/").filter(Boolean);
971
+ const resolvedPath = path.resolve(vaultPath, ...relativePath);
972
+ const normalizedVaultPath = path.resolve(vaultPath);
973
+ if (resolvedPath !== normalizedVaultPath && !resolvedPath.startsWith(`${normalizedVaultPath}${path.sep}`)) throw new Error(`Resolved path escapes the vault root: ${targetPath}`);
974
+ return resolvedPath;
975
+ }
976
+ //#endregion
977
+ //#region src/vault/vault.ts
978
+ function createVaultApi(options) {
979
+ const scopeRoot = normalizeScope(options.root);
980
+ return {
981
+ async delete(targetPath, deleteOptions = {}) {
982
+ await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
983
+ force: true,
984
+ recursive: true
985
+ });
986
+ if (deleteOptions.permanent === false) return;
987
+ },
988
+ async exists(targetPath) {
989
+ try {
990
+ await access(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath));
991
+ return true;
992
+ } catch {
993
+ return false;
994
+ }
995
+ },
996
+ json(targetPath) {
997
+ const jsonFile = {
998
+ async patch(updater) {
999
+ const currentValue = await jsonFile.read();
1000
+ const draft = structuredClone(currentValue);
1001
+ const nextValue = await updater(draft) ?? draft;
1002
+ await jsonFile.write(nextValue);
1003
+ return nextValue;
1004
+ },
1005
+ async read() {
1006
+ const rawValue = await readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
1007
+ return JSON.parse(rawValue);
1008
+ },
1009
+ async write(value) {
1010
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1011
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
1012
+ await writeFile(resolvedPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
1013
+ }
1014
+ };
1015
+ return jsonFile;
1016
+ },
1017
+ async mkdir(targetPath) {
1018
+ await mkdir(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), { recursive: true });
1019
+ },
1020
+ async read(targetPath) {
1021
+ return readFile(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), "utf8");
1022
+ },
1023
+ async waitForContent(targetPath, predicate, waitOptions = {}) {
1024
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1025
+ return options.obsidian.waitFor(async () => {
1026
+ try {
1027
+ const content = await readFile(resolvedPath, "utf8");
1028
+ return await predicate(content) ? content : false;
1029
+ } catch {
1030
+ return false;
1031
+ }
1032
+ }, {
1033
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to match content`,
1034
+ ...waitOptions
1035
+ });
1036
+ },
1037
+ async waitForExists(targetPath, waitOptions) {
1038
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1039
+ await options.obsidian.waitFor(async () => {
1040
+ try {
1041
+ await access(resolvedPath);
1042
+ return true;
1043
+ } catch {
1044
+ return false;
1045
+ }
1046
+ }, {
1047
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to exist`,
1048
+ ...waitOptions
1049
+ });
1050
+ },
1051
+ async waitForMissing(targetPath, waitOptions) {
1052
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1053
+ await options.obsidian.waitFor(async () => {
1054
+ try {
1055
+ await access(resolvedPath);
1056
+ return false;
1057
+ } catch {
1058
+ return true;
1059
+ }
1060
+ }, {
1061
+ message: `vault path "${resolveVaultPath(scopeRoot, targetPath)}" to be removed`,
1062
+ ...waitOptions
1063
+ });
1064
+ },
1065
+ async write(targetPath, content, writeOptions = {}) {
1066
+ const resolvedPath = await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath);
1067
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
1068
+ await writeFile(resolvedPath, content, "utf8");
1069
+ if (!writeOptions.waitForContent) return;
1070
+ const predicate = typeof writeOptions.waitForContent === "function" ? writeOptions.waitForContent : (value) => value === content;
1071
+ await this.waitForContent(targetPath, predicate, writeOptions.waitOptions);
1072
+ }
1073
+ };
1074
+ }
1075
+ //#endregion
521
1076
  //#region src/artifacts/failure-artifacts.ts
522
1077
  const DEFAULT_FAILURE_ARTIFACTS_DIR = ".obsidian-e2e-artifacts";
1078
+ const DEFAULT_FAILURE_ARTIFACT_CAPTURE = {
1079
+ activeFile: true,
1080
+ activeNote: true,
1081
+ consoleMessages: true,
1082
+ dom: true,
1083
+ editorText: true,
1084
+ notices: true,
1085
+ parsedFrontmatter: true,
1086
+ runtimeErrors: true,
1087
+ screenshot: true,
1088
+ tabs: true,
1089
+ workspace: true
1090
+ };
523
1091
  function getFailureArtifactConfig(options) {
524
1092
  if (!options.captureOnFailure) return {
525
1093
  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
- },
1094
+ capture: { ...DEFAULT_FAILURE_ARTIFACT_CAPTURE },
534
1095
  enabled: false
535
1096
  };
536
1097
  const overrides = options.captureOnFailure === true ? {} : options.captureOnFailure;
537
1098
  return {
538
1099
  artifactsDir: path.resolve(options.artifactsDir ?? ".obsidian-e2e-artifacts"),
539
1100
  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
1101
+ ...DEFAULT_FAILURE_ARTIFACT_CAPTURE,
1102
+ ...overrides
546
1103
  },
547
1104
  enabled: true
548
1105
  };
549
1106
  }
550
1107
  function getFailureArtifactDirectory(artifactsDir, task) {
551
1108
  const suffix = task.id.split("_").at(-1) ?? "test";
552
- return path.join(artifactsDir, `${sanitizeForPath(task.name)}-${suffix}`);
1109
+ return path.join(artifactsDir, `${sanitizePathSegment(task.name, { maxLength: 60 })}-${suffix}`);
553
1110
  }
554
1111
  async function captureFailureArtifacts(task, obsidian, options) {
555
1112
  const config = getFailureArtifactConfig(options);
556
1113
  if (!config.enabled) return;
557
1114
  const artifactDirectory = getFailureArtifactDirectory(config.artifactsDir, task);
558
1115
  await mkdir(artifactDirectory, { recursive: true });
1116
+ const activeFile = readArtifactInput(() => readActiveFilePath(obsidian));
1117
+ const activeNote = readArtifactInput(async () => {
1118
+ const activeFilePath = await unwrapArtifactInput(activeFile);
1119
+ return activeFilePath ? readActiveNoteSnapshot(obsidian, activeFilePath) : null;
1120
+ });
1121
+ const diagnostics = await obsidian.dev.diagnostics().catch(() => null);
559
1122
  await Promise.all([
560
- captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await obsidian.dev.eval("app.workspace.getActiveFile()?.path ?? null") })),
1123
+ captureJsonArtifact(artifactDirectory, "active-file.json", config.capture.activeFile, async () => ({ activeFile: await unwrapArtifactInput(activeFile) })),
1124
+ captureTextArtifact(artifactDirectory, "active-note.md", config.capture.activeNote, async () => (await unwrapArtifactInput(activeNote))?.raw ?? ""),
1125
+ captureJsonArtifact(artifactDirectory, "active-note-frontmatter.json", config.capture.parsedFrontmatter, async () => ({ frontmatter: (await unwrapArtifactInput(activeNote))?.frontmatter ?? null })),
561
1126
  captureTextArtifact(artifactDirectory, "dom.txt", config.capture.dom, async () => String(await obsidian.dev.dom({
562
1127
  inner: true,
563
1128
  selector: ".workspace"
564
1129
  }))),
565
- captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.eval("app.workspace.activeLeaf?.view?.editor?.getValue?.() ?? null") })),
1130
+ captureJsonArtifact(artifactDirectory, "editor.json", config.capture.editorText, async () => ({ text: await obsidian.dev.editorText() })),
1131
+ captureJsonArtifact(artifactDirectory, "console-messages.json", config.capture.consoleMessages, async () => diagnostics?.consoleMessages ?? []),
1132
+ captureJsonArtifact(artifactDirectory, "runtime-errors.json", config.capture.runtimeErrors, async () => diagnostics?.runtimeErrors ?? []),
1133
+ captureJsonArtifact(artifactDirectory, "notices.json", config.capture.notices, async () => diagnostics?.notices ?? []),
566
1134
  captureScreenshotArtifact(artifactDirectory, config.capture.screenshot, obsidian),
567
1135
  captureJsonArtifact(artifactDirectory, "tabs.json", config.capture.tabs, () => obsidian.tabs()),
568
1136
  captureJsonArtifact(artifactDirectory, "workspace.json", config.capture.workspace, () => obsidian.workspace()),
@@ -570,14 +1138,6 @@ async function captureFailureArtifacts(task, obsidian, options) {
570
1138
  ]);
571
1139
  return artifactDirectory;
572
1140
  }
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
1141
  async function captureJsonArtifact(artifactDirectory, filename, enabled, readValue) {
582
1142
  if (!enabled) return;
583
1143
  try {
@@ -607,8 +1167,68 @@ async function captureTextArtifact(artifactDirectory, filename, enabled, readVal
607
1167
  function formatArtifactError(error) {
608
1168
  return error instanceof Error ? `${error.name}: ${error.message}\n` : `${String(error)}\n`;
609
1169
  }
610
- function sanitizeForPath(value) {
611
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "test";
1170
+ async function readActiveFilePath(obsidian) {
1171
+ return obsidian.dev.activeFilePath();
1172
+ }
1173
+ async function readActiveNoteSnapshot(obsidian, activeFile) {
1174
+ return parseNoteDocument(await createVaultApi({ obsidian }).read(activeFile));
1175
+ }
1176
+ function readArtifactInput(readValue) {
1177
+ return { promise: readValue().then((value) => ({
1178
+ ok: true,
1179
+ value
1180
+ }), (error) => ({
1181
+ error,
1182
+ ok: false
1183
+ })) };
1184
+ }
1185
+ async function unwrapArtifactInput(result) {
1186
+ const value = await result.promise;
1187
+ if (!value.ok) throw value.error;
1188
+ return value.value;
1189
+ }
1190
+ //#endregion
1191
+ //#region src/vault/sandbox.ts
1192
+ async function createSandboxApi(options) {
1193
+ const root = posix.join(options.sandboxRoot, `${sanitizePathSegment(options.testName)}-${randomUUID().slice(0, 8)}`);
1194
+ const sandboxPath = (...segments) => posix.join(root, ...segments);
1195
+ const vault = createVaultApi({
1196
+ obsidian: options.obsidian,
1197
+ root
1198
+ });
1199
+ await vault.mkdir(".");
1200
+ return {
1201
+ ...vault,
1202
+ async cleanup() {
1203
+ await vault.delete(".", { permanent: true });
1204
+ },
1205
+ async frontmatter(targetPath) {
1206
+ return options.obsidian.metadata.frontmatter(sandboxPath(targetPath));
1207
+ },
1208
+ path(...segments) {
1209
+ return sandboxPath(...segments);
1210
+ },
1211
+ async readNote(targetPath) {
1212
+ return parseNoteDocument(await vault.read(targetPath));
1213
+ },
1214
+ root,
1215
+ async waitForFrontmatter(targetPath, predicate, waitOptions) {
1216
+ return options.obsidian.metadata.waitForFrontmatter(sandboxPath(targetPath), predicate, waitOptions);
1217
+ },
1218
+ async waitForMetadata(targetPath, predicate, waitOptions) {
1219
+ return options.obsidian.metadata.waitForMetadata(sandboxPath(targetPath), predicate, waitOptions);
1220
+ },
1221
+ async writeNote(writeOptions) {
1222
+ const { path, waitForMetadata = true, waitOptions, ...noteInput } = writeOptions;
1223
+ const document = createNoteDocument(noteInput);
1224
+ await vault.write(path, document.raw);
1225
+ if (waitForMetadata) {
1226
+ const predicate = typeof waitForMetadata === "function" ? waitForMetadata : void 0;
1227
+ await options.obsidian.metadata.waitForMetadata(sandboxPath(path), predicate, waitOptions);
1228
+ }
1229
+ return document;
1230
+ }
1231
+ };
612
1232
  }
613
1233
  //#endregion
614
1234
  //#region src/fixtures/vault-lock.ts
@@ -772,147 +1392,167 @@ async function sleep(durationMs) {
772
1392
  async function writeMetadata(metadataPath, metadata) {
773
1393
  await writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
774
1394
  }
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);
1395
+ function createBaseFixtures(options, fixtureOptions = {}) {
1396
+ const createVault = fixtureOptions.createVault ?? ((obsidian) => createVaultApi({ obsidian }));
797
1397
  return {
798
- async delete(targetPath, deleteOptions = {}) {
799
- await rm(await resolveFilesystemPath(options.obsidian, scopeRoot, targetPath), {
800
- force: true,
801
- recursive: true
1398
+ _vaultLock: [async ({}, use) => {
1399
+ if (!options.sharedVaultLock) {
1400
+ await use(null);
1401
+ return;
1402
+ }
1403
+ const lockClient = createObsidianClient(options);
1404
+ await lockClient.verify();
1405
+ const vaultLock = await acquireVaultRunLock({
1406
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1407
+ vaultName: options.vault,
1408
+ vaultPath: await lockClient.vaultPath()
802
1409
  });
803
- if (deleteOptions.permanent === false) return;
804
- },
805
- async exists(targetPath) {
1410
+ await vaultLock.publishMarker(lockClient);
806
1411
  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 () => {
1412
+ await use(vaultLock);
1413
+ } finally {
843
1414
  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
1415
+ await clearVaultRunLockMarker(lockClient);
1416
+ } catch {}
1417
+ await vaultLock.release();
1418
+ }
1419
+ }, { scope: "worker" }],
1420
+ _testContext: async ({ _vaultLock, onTestFailed, task }, use) => {
1421
+ let failedTask = false;
1422
+ onTestFailed(() => {
1423
+ failedTask = true;
852
1424
  });
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
1425
+ const context = await createInternalTestContext({
1426
+ ...options,
1427
+ createVault,
1428
+ testName: task.name,
1429
+ vaultLock: _vaultLock
866
1430
  });
1431
+ try {
1432
+ await use(context);
1433
+ } finally {
1434
+ await context.cleanup({ failedTask: failedTask ? task : void 0 });
1435
+ }
867
1436
  },
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
- });
1437
+ obsidian: async ({ _testContext }, use) => {
1438
+ await use(_testContext.obsidian);
881
1439
  },
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);
1440
+ sandbox: async ({ _testContext }, use) => {
1441
+ await use(_testContext.sandbox);
1442
+ },
1443
+ vault: async ({ _testContext }, use) => {
1444
+ await use(_testContext.vault);
889
1445
  }
890
1446
  };
891
1447
  }
892
1448
  //#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
- };
1449
+ //#region src/fixtures/test-context.ts
1450
+ async function createTestContext(options) {
1451
+ return createInternalTestContext(options);
1452
+ }
1453
+ async function withVaultSandbox(options, run) {
1454
+ const context = await createInternalTestContext(options);
1455
+ try {
1456
+ return await run(context);
1457
+ } finally {
1458
+ await context.cleanup();
1459
+ }
1460
+ }
1461
+ async function createInternalTestContext(options) {
1462
+ const obsidian = createObsidianClient(options);
1463
+ const trackedPlugins = /* @__PURE__ */ new Map();
1464
+ let sandbox = null;
1465
+ const vaultLock = options.vaultLock ?? await maybeAcquireVaultLock(options, obsidian);
1466
+ const ownsVaultLock = options.vaultLock === void 0 && vaultLock !== null;
1467
+ const vaultFactory = options.createVault ?? ((client) => createVaultApi({ obsidian: client }));
1468
+ let disposed = false;
1469
+ try {
1470
+ await obsidian.verify();
1471
+ if (vaultLock) await vaultLock.publishMarker(obsidian);
1472
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1473
+ const vault = await vaultFactory(obsidian);
1474
+ sandbox = await createSandboxApi({
1475
+ obsidian,
1476
+ sandboxRoot: options.sandboxRoot ?? "__obsidian_e2e__",
1477
+ testName: options.testName ?? "test"
1478
+ });
1479
+ const captureArtifacts = async (task) => captureFailureArtifacts(task, obsidian, {
1480
+ ...options,
1481
+ plugin: trackedPlugins.size === 1 ? [...trackedPlugins.values()][0].plugin : void 0
1482
+ });
1483
+ const cleanup = async (cleanupOptions = {}) => {
1484
+ if (disposed) return;
1485
+ disposed = true;
1486
+ const cleanupErrors = [];
1487
+ try {
1488
+ if (cleanupOptions.failedTask && options.captureOnFailure) await captureArtifacts(cleanupOptions.failedTask);
1489
+ } finally {
1490
+ try {
1491
+ await getClientInternals(obsidian).restoreAll();
1492
+ } finally {
1493
+ for (const session of [...trackedPlugins.values()].reverse()) if (!session.wasEnabled) await recordCleanupError(cleanupErrors, async () => session.plugin.disable({ filter: session.filter }));
1494
+ try {
1495
+ await clearVaultRunLockMarker(obsidian);
1496
+ } catch {}
1497
+ if (ownsVaultLock) await recordCleanupError(cleanupErrors, async () => vaultLock.release());
1498
+ await recordCleanupError(cleanupErrors, async () => sandbox.cleanup());
1499
+ }
1500
+ }
1501
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
1502
+ if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "One or more test cleanup steps failed.");
1503
+ };
1504
+ return {
1505
+ obsidian,
1506
+ sandbox,
1507
+ vault,
1508
+ captureFailureArtifacts: captureArtifacts,
1509
+ cleanup,
1510
+ async plugin(id, sessionOptions = {}) {
1511
+ const existing = trackedPlugins.get(id);
1512
+ if (existing) return existing.plugin;
1513
+ const plugin = obsidian.plugin(id);
1514
+ const wasEnabled = await plugin.isEnabled();
1515
+ if (!wasEnabled) await plugin.enable({ filter: sessionOptions.filter });
1516
+ if (sessionOptions.seedData !== void 0) await plugin.data().write(sessionOptions.seedData);
1517
+ trackedPlugins.set(id, {
1518
+ filter: sessionOptions.filter,
1519
+ plugin,
1520
+ wasEnabled
1521
+ });
1522
+ return plugin;
1523
+ },
1524
+ async resetDiagnostics() {
1525
+ await obsidian.dev.resetDiagnostics().catch(() => {});
1526
+ }
1527
+ };
1528
+ } catch (error) {
1529
+ try {
1530
+ if (sandbox) await sandbox.cleanup();
1531
+ } finally {
1532
+ try {
1533
+ await clearVaultRunLockMarker(obsidian);
1534
+ } catch {}
1535
+ if (ownsVaultLock) await vaultLock.release();
1536
+ }
1537
+ throw error;
1538
+ }
911
1539
  }
912
- function sanitizeSegment(value) {
913
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "test";
1540
+ async function recordCleanupError(cleanupErrors, run) {
1541
+ try {
1542
+ await run();
1543
+ } catch (error) {
1544
+ cleanupErrors.push(error);
1545
+ }
1546
+ }
1547
+ async function maybeAcquireVaultLock(options, obsidian) {
1548
+ if (!options.sharedVaultLock) return null;
1549
+ return acquireVaultRunLock({
1550
+ ...options.sharedVaultLock === true ? {} : options.sharedVaultLock,
1551
+ vaultName: options.vault,
1552
+ vaultPath: await obsidian.vaultPath()
1553
+ });
914
1554
  }
915
1555
  //#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 };
1556
+ 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
1557
 
918
- //# sourceMappingURL=sandbox--mUbNsh7.mjs.map
1558
+ //# sourceMappingURL=test-context-BprSx6U1.mjs.map